Improved search filtering (#940)

* Filter search by collectionId

* Improve spec, remove recursive import

* Add userId filter for documents.search

* 💚

* Search filter UI

* WIP UI

* Date filtering
Prevent dupe menu

* Refactor

* button

* Added year option, improved hover states

* Add new indexes

* Remove manual string interpolation in SQL construction

* Move dateFilter validation to controller

* Fixes: Double query when changing filter
Fixes: Visual jump between filters in dropdown

* Add option to clear filters

* More clearly define dropdowns in dark mode

* Checkbox -> Checkmark
This commit is contained in:
Tom Moor
2019-04-23 07:31:20 -07:00
committed by GitHub
parent a256eba856
commit da7fdfef0a
23 changed files with 679 additions and 76 deletions

View File

@@ -0,0 +1,39 @@
// @flow
import * as React from 'react';
import { observer, inject } from 'mobx-react';
import FilterOptions from './FilterOptions';
import CollectionsStore from 'stores/CollectionsStore';
const defaultOption = {
key: undefined,
label: 'Any collection',
};
type Props = {
collections: CollectionsStore,
collectionId: ?string,
onSelect: (key: ?string) => void,
};
@observer
class CollectionFilter extends React.Component<Props> {
render() {
const { onSelect, collectionId, collections } = this.props;
const collectionOptions = collections.orderedData.map(user => ({
key: user.id,
label: user.name,
}));
return (
<FilterOptions
options={[defaultOption, ...collectionOptions]}
activeKey={collectionId}
onSelect={onSelect}
defaultLabel="Any collection"
selectedPrefix="Collection:"
/>
);
}
}
export default inject('collections')(CollectionFilter);

View File

@@ -0,0 +1,29 @@
// @flow
import * as React from 'react';
import FilterOptions from './FilterOptions';
const options = [
{ key: undefined, label: 'Any time' },
{ key: 'day', label: 'Past day' },
{ key: 'week', label: 'Past week' },
{ key: 'month', label: 'Past month' },
{ key: 'year', label: 'Past year' },
];
type Props = {
dateFilter: ?string,
onSelect: (key: ?string) => void,
};
const DateFilter = ({ dateFilter, onSelect }: Props) => {
return (
<FilterOptions
options={options}
activeKey={dateFilter}
onSelect={onSelect}
defaultLabel="Any time"
/>
);
};
export default DateFilter;

View File

@@ -0,0 +1,59 @@
// @flow
import * as React from 'react';
import { CheckmarkIcon } from 'outline-icons';
import styled from 'styled-components';
import HelpText from 'components/HelpText';
import Flex from 'shared/components/Flex';
type Props = {
label: string,
note?: string,
onSelect: () => void,
active: boolean,
};
const FilterOption = ({ label, note, onSelect, active }: Props) => {
return (
<ListItem active={active}>
<Anchor onClick={active ? undefined : onSelect}>
<Flex align="center" justify="space-between">
<span>
{label}
{note && <HelpText small>{note}</HelpText>}
</span>
{active && <Checkmark />}
</Flex>
</Anchor>
</ListItem>
);
};
const Checkmark = styled(CheckmarkIcon)`
flex-shrink: 0;
padding-left: 4px;
fill: ${props => props.theme.text};
`;
const Anchor = styled('a')`
display: flex;
flex-direction: column;
font-size: 15px;
padding: 4px 8px;
color: ${props => props.theme.text};
min-height: 32px;
${HelpText} {
font-weight: normal;
}
&:hover {
background: ${props => props.theme.listItemHoverBackground};
}
`;
const ListItem = styled('li')`
list-style: none;
font-weight: ${props => (props.active ? '600' : 'normal')};
`;
export default FilterOption;

View File

@@ -0,0 +1,99 @@
// @flow
import * as React from 'react';
import { find } from 'lodash';
import styled from 'styled-components';
import Scrollable from 'components/Scrollable';
import Button from 'components/Button';
import { DropdownMenu } from 'components/DropdownMenu';
import FilterOption from './FilterOption';
type Props = {
options: {
key: ?string,
label: string,
note?: string,
}[],
activeKey: ?string,
defaultLabel?: string,
selectedPrefix?: string,
onSelect: (key: ?string) => void,
};
const FilterOptions = ({
options,
activeKey,
defaultLabel,
selectedPrefix = '',
onSelect,
}: Props) => {
const selected = find(options, { key: activeKey }) || options[0];
const selectedLabel = selected ? `${selectedPrefix} ${selected.label}` : '';
return (
<DropdownButton label={activeKey ? selectedLabel : defaultLabel}>
<List>
{options.map(option => (
<FilterOption
key={option.key}
onSelect={() => onSelect(option.key)}
active={option.key === activeKey}
{...option}
/>
))}
</List>
</DropdownButton>
);
};
const Content = styled('div')`
padding: 0 8px;
width: 250px;
max-height: 50vh;
p {
margin-bottom: 0;
}
`;
const StyledButton = styled(Button)`
box-shadow: none;
text-transform: none;
border-color: transparent;
height: 28px;
&:hover {
background: transparent;
}
`;
const SearchFilter = props => {
return (
<DropdownMenu
className={props.className}
label={
<StyledButton neutral disclosure small>
{props.label}
</StyledButton>
}
leftAlign
>
{({ closePortal }) => (
<Content>
<Scrollable>{props.children}</Scrollable>
</Content>
)}
</DropdownMenu>
);
};
const DropdownButton = styled(SearchFilter)`
margin-right: 8px;
`;
const List = styled('ol')`
list-style: none;
margin: 0;
padding: 0;
`;
export default FilterOptions;

View File

@@ -0,0 +1,34 @@
// @flow
import * as React from 'react';
import FilterOptions from './FilterOptions';
const options = [
{
key: undefined,
label: 'Active documents',
note: 'Documents in collections you are able to access',
},
{
key: 'true',
label: 'All documents',
note: 'Include documents that are in the archive',
},
];
type Props = {
includeArchived: boolean,
onSelect: (key: ?string) => void,
};
const StatusFilter = ({ includeArchived, onSelect }: Props) => {
return (
<FilterOptions
options={options}
activeKey={includeArchived ? 'true' : undefined}
onSelect={onSelect}
defaultLabel="Active documents"
/>
);
};
export default StatusFilter;

View File

@@ -0,0 +1,43 @@
// @flow
import * as React from 'react';
import { observer, inject } from 'mobx-react';
import FilterOptions from './FilterOptions';
import UsersStore from 'stores/UsersStore';
const defaultOption = {
key: undefined,
label: 'Any author',
};
type Props = {
users: UsersStore,
userId: ?string,
onSelect: (key: ?string) => void,
};
@observer
class UserFilter extends React.Component<Props> {
componentDidMount() {
this.props.users.fetchPage({ limit: 100 });
}
render() {
const { onSelect, userId, users } = this.props;
const userOptions = users.orderedData.map(user => ({
key: user.id,
label: user.name,
}));
return (
<FilterOptions
options={[defaultOption, ...userOptions]}
activeKey={userId}
onSelect={onSelect}
defaultLabel="Any author"
selectedPrefix="Author:"
/>
);
}
}
export default inject('users')(UserFilter);