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

A massive redesign of the advanced search form #1701

Merged
merged 25 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9723002
Create an Advanced Search Form layout
davidmz Oct 24, 2024
0c9445b
Parse and build search query
davidmz Oct 24, 2024
1f75f8e
Fix the autocomplete pattern search logic for the zero-width pattern …
davidmz Oct 25, 2024
c004585
Add autocomplete and make inactive options gray
davidmz Oct 25, 2024
a3a09fa
Adapt styles for dark mode
davidmz Oct 25, 2024
dd23267
Remove previous advanced search form
davidmz Oct 25, 2024
1f44651
Add "Advanced search options" dropdown
davidmz Oct 25, 2024
47a02c5
Reword form text
davidmz Oct 26, 2024
d23073e
Make 'select' paddings consistent with the other inputs
davidmz Oct 26, 2024
3fcdc94
Add custom placeholder to the top search form
davidmz Oct 27, 2024
c6ed756
Focus top search form on [/] or Ctrl+[K] keys
davidmz Oct 27, 2024
db1f0e1
Fix the focused advanced search button color
davidmz Oct 27, 2024
3fd19a0
Update changelog
davidmz Oct 27, 2024
deecdcf
Update the search form placeholder
davidmz Oct 30, 2024
cb7acfc
Move top search form to the separate component
davidmz Oct 30, 2024
35db1a3
Add a separate button to the "What to search" input
davidmz Oct 30, 2024
0e980bb
Fix 'legend' color in dark mode
davidmz Oct 31, 2024
4da16f3
Add missed usernameFilters
davidmz Oct 31, 2024
e73f583
Allow to click on resulting search query
davidmz Oct 31, 2024
5c207bc
Make the behavior of the top search form the same on all pages
davidmz Oct 31, 2024
8298087
Use proper onBlur (focusout) handler in top search form
davidmz Oct 31, 2024
242e9d0
Place the exact match on top of the autocomplete variants
davidmz Oct 31, 2024
83d20d4
Submit the advanced search form by Enter on text fields
davidmz Oct 31, 2024
bd776c4
Add "Exclude posts liked by users" filter to the advanced form
davidmz Oct 31, 2024
b610d8f
Use regular Link in the "Advanced search" dropdown
davidmz Nov 7, 2024
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.136.0] - Not released
### Changed
- The advanced search form is refactored to support the new search operators.
### Added
- When user presses '/' or 'Ctrl+K' hotkeys, the search input form in the
header is focused.
- This form now has an "Advanced search options" button that redirects to the
advanced search form.

## [1.135.3] - 2024-10-22
### Fixed
Expand Down
292 changes: 292 additions & 0 deletions src/components/advanced-search-form/advanced-search-form.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
import cn from 'classnames';
import { browserHistory } from 'react-router';
import { useEffect, useMemo, useReducer, useState } from 'react';
import { useEvent } from 'react-use-event-hook';
import { faChevronDown } from '@fortawesome/free-solid-svg-icons';
import { useMediaQuery } from '../hooks/media-query';
import { ButtonLink } from '../button-link';
import { Icon } from '../fontawesome-icons';
import { useSearchQuery } from '../hooks/search-query';
import style from './advanced-search-form.module.scss';
import { BoolInput } from './bool-input';
import { ChooseInput } from './choose-input';
import { Columns } from './columns';
import { IntervalInput } from './interval-input';
import { Section } from './section';
import { TextInput } from './text-input';
import {
intervalFilters,
parseQuery,
useCheckboxField,
usernameFilters,
usernames,
useTextField,
} from './helpers';
import { reducer } from './reducer';
import { filtersContext } from './context';

export function AdvancedSearchForm() {
const isWideScreen = useMediaQuery('(min-width: 768px)');
const [formExpanded, setFormExpanded] = useState(false);
const [docsExpanded, setDocsExpanded] = useState(false);

const expandForm = useEvent(() => setFormExpanded(true));
const expandDocs = useEvent(() => setDocsExpanded(true));

useEffect(() => {
if (isWideScreen) {
setFormExpanded(false);
setDocsExpanded(false);
}
}, [isWideScreen]);

const showFullForm = isWideScreen || formExpanded;
const showFullDocs = isWideScreen || docsExpanded;

const initialQuery = parseQuery(useSearchQuery());

const [query, queryAttrs] = useTextField(initialQuery.text);
const [inPosts, inPostsAttrs] = useCheckboxField(initialQuery.inPosts);
const [inComments, inCommentsAttrs] = useCheckboxField(initialQuery.inComments);

const [filters, dispatch] = useReducer(reducer, initialQuery.filters);
const ctxValue = useMemo(() => [filters, dispatch], [filters, dispatch]);

const resultingQuery = useMemo(() => {
if (!inPosts && !inComments) {
return '';
}
return [
inPosts && !inComments ? 'in-body:' : '',
inComments && !inPosts ? 'in-comments:' : '',
query.trim(),
...Object.entries(filters).map(([k, v]) => {
if (intervalFilters.has(k) && !/\d/.test(v)) {
return null;
}
if (usernameFilters.has(k)) {
v = usernames(v);
}
return k + v;
}),
]
.filter(Boolean)
.join(' ');
}, [inPosts, inComments, query, filters]);

const onSearch = useEvent(() =>
browserHistory.push(`/search?q=${encodeURIComponent(resultingQuery)}`),
);

const onKeyDown = useEvent((e) => {
if (
e.code === 'Enter' &&
e.target.matches('input[type="text"], input[type="date"], input[type="search"]')
) {
e.preventDefault();
onSearch();
}
});

return (
<filtersContext.Provider value={ctxValue}>
<div className={style.form} onKeyDown={onKeyDown}>
<Section title="What to search">
<div className={style.searchInputBox}>
<input
type="search"
name="q"
{...queryAttrs}
className="form-control"
placeholder="Text to search"
/>
<button
type="button"
disabled={resultingQuery === ''}
className={cn('btn btn-primary')}
onClick={onSearch}
>
Search
</button>
</div>
<div className={style.searchScopes}>
Search for:
<label>
<input type="checkbox" {...inPostsAttrs} /> posts
</label>
<label>
<input type="checkbox" {...inCommentsAttrs} /> comments
</label>
</div>
</Section>
<Section title="Search only in">
<Columns>
<BoolInput
label="My friends and groups"
filter="in-my:friends"
value={filters['in-my:friends']}
/>
<BoolInput label="My discussions" filter="in-my:discussions" />
<BoolInput label="My posts" filter="from:me" />
<BoolInput label="My direct messages" filter="in-my:messages" />
<BoolInput label="My saved posts" filter="in-my:saves" />
<BoolInput label="All content written by me" filter="by:me" />
</Columns>
</Section>
<Section title="With conditions">
<Columns>
<TextInput label="Group/user feeds" placeholder="group1, user2" filter="in:" />
<TextInput label="Content written by users" placeholder="user1, user2" filter="by:" />
<TextInput label="Posts written by users" placeholder="user1, user2" filter="from:" />
<TextInput
label="Posts commented by users"
placeholder="user1, user2"
filter="commented-by:"
/>
<TextInput label="Posts created after" type="date" filter="post-date:>=" />
<TextInput label="Posts created before" type="date" filter="post-date:<" />
{showFullForm ? (
<>
<ChooseInput label="Posts with privacy" filter="is:">
<option value="">Any</option>
<option value="public">Public</option>
<option value="protected">Protected</option>
<option value="private">Private</option>
</ChooseInput>
<ChooseInput label="Posts with attachments" filter="has:">
<option value="">With or without</option>
<option value="images">Images</option>
<option value="audio">Audio</option>
<option value="files">Any files</option>
</ChooseInput>
<IntervalInput label="Post comments count" filter="comments:" />
<TextInput
label="Posts liked by users"
placeholder="user1, user2"
filter="liked-by:"
/>
<IntervalInput label="Post likes count" filter="likes:" />
<TextInput
label="Comments liked by users"
placeholder="user1, user2"
filter="cliked-by:"
/>
<IntervalInput label="Comment likes count" filter="clikes:" />
</>
) : null}
</Columns>
</Section>
{showFullForm ? (
<Section title="Exclusions">
<Columns>
<TextInput
label="Exclude posts written by users"
placeholder="user1, user2"
filter="-from:"
/>
<TextInput
label="Exclude group/user feeds"
placeholder="group1, user2"
filter="-in:"
/>
<TextInput
label="Exclude posts commented by users"
placeholder="user1, user2"
filter="-commented-by:"
/>
<TextInput
label="Exclude posts liked by users"
placeholder="user1, user2"
filter="-liked-by:"
/>
<ChooseInput label="Exclude posts with attachments" filter="-has:">
<option value="">Don&#x2019;t exclude</option>
<option value="images">Images</option>
<option value="audio">Audio</option>
<option value="files">Any files</option>
</ChooseInput>
<ChooseInput label="Exclude posts with privacy" filter="-is:">
<option value="">Any</option>
<option value="public">Public</option>
<option value="protected">Protected</option>
<option value="private">Private</option>
</ChooseInput>
</Columns>
</Section>
) : null}
{!showFullForm ? (
<p>
<ButtonLink onClick={expandForm}>
<Icon icon={faChevronDown} className={style.expandIcon} /> Show all conditions
</ButtonLink>
</p>
) : null}
<Section sticky>
<button
type="button"
disabled={resultingQuery === ''}
className={cn('btn btn-primary', style.bigSearchButton)}
onClick={onSearch}
>
Search
</button>
</Section>
{resultingQuery ? (
<p>
Search query:{' '}
<ButtonLink tag="code" onClick={onSearch}>
{resultingQuery}
</ButtonLink>
</p>
) : null}
{showFullDocs ? (
<>
{' '}
<p>
Use double-quotes to search words in the exact form and specific word order:{' '}
<em>&quot;freefeed version&quot;</em>
<br />
Use the asterisk symbol (<code>*</code>) to search word by prefix: <em>free*</em>. The
minimum prefix length is two characters.
<br />
Use the pipe symbol (<code>|</code>) between words to search any of them:{' '}
<em>freefeed | version</em>
<br />
Use the minus sign (<code>-</code>) to exclude some word from search results:{' '}
<em>freefeed -version</em>
<br />
Use the plus sign (<code>+</code>) to specify word order: <em>freefeed + version</em>
<br />
</p>
<p>
Learn the{' '}
<a
href="https://github.com/FreeFeed/freefeed-server/wiki/FreeFeed-Search"
target="_blank"
>
full query syntax
</a>{' '}
for more advanced search requests.
</p>
</>
) : (
<>
<p>
Use double-quotes to search words in the exact form and specific word order:{' '}
<em>&quot;freefeed version&quot;</em>
<br />
Use the asterisk symbol (<code>*</code>) to search word by prefix: <em>free*</em>. The
minimum prefix length is two characters.
</p>
<p>
<ButtonLink onClick={expandDocs}>
<Icon icon={faChevronDown} className={style.expandIcon} /> Show search query syntax
help
</ButtonLink>
</p>
</>
)}
</div>
</filtersContext.Provider>
);
}
Loading
Loading