Search archived documents (#932)

* POC

* Improved styling

* Test
This commit is contained in:
Tom Moor 2019-04-09 09:20:30 -07:00 committed by GitHub
parent 57e051d62b
commit c1256c61aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 89 additions and 34 deletions

View File

@ -8,15 +8,18 @@ export type Props = {
label?: string,
className?: string,
note?: string,
small?: boolean,
};
const LabelText = styled.span`
font-weight: 500;
margin-left: 10px;
margin-left: ${props => (props.small ? '6px' : '10px')};
${props => (props.small ? `color: ${props.theme.textSecondary}` : '')};
`;
const Wrapper = styled.div`
padding-bottom: 8px;
${props => (props.small ? 'font-size: 14px' : '')};
`;
const Label = styled.label`
@ -28,15 +31,16 @@ export default function Checkbox({
label,
note,
className,
small,
short,
...rest
}: Props) {
return (
<React.Fragment>
<Wrapper>
<Wrapper small={small}>
<Label>
<input type="checkbox" {...rest} />
{label && <LabelText>{label}</LabelText>}
{label && <LabelText small={small}>{label}</LabelText>}
</Label>
{note && <HelpText small>{note}</HelpText>}
</Wrapper>

View File

@ -18,6 +18,8 @@ import { meta } from 'utils/keyboard';
import Flex from 'shared/components/Flex';
import Empty from 'components/Empty';
import Fade from 'components/Fade';
import Checkbox from 'components/Checkbox';
import HelpText from 'components/HelpText';
import CenteredContent from 'components/CenteredContent';
import LoadingIndicator from 'components/LoadingIndicator';
@ -33,33 +35,6 @@ type Props = {
notFound: ?boolean,
};
const Container = styled(CenteredContent)`
> div {
position: relative;
height: 100%;
}
`;
const ResultsWrapper = styled(Flex)`
position: absolute;
transition: all 300ms cubic-bezier(0.65, 0.05, 0.36, 1);
top: ${props => (props.pinToTop ? '0%' : '50%')};
margin-top: ${props => (props.pinToTop ? '40px' : '-75px')};
width: 100%;
`;
const ResultList = styled(Flex)`
margin-bottom: 150px;
opacity: ${props => (props.visible ? '1' : '0')};
transition: all 400ms cubic-bezier(0.65, 0.05, 0.36, 1);
`;
const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)`
display: flex;
flex-direction: column;
flex: 1;
`;
@observer
class Search extends React.Component<Props> {
firstDocument: ?DocumentPreview;
@ -68,6 +43,7 @@ class Search extends React.Component<Props> {
@observable offset: number = 0;
@observable allowLoadMore: boolean = true;
@observable isFetching: boolean = false;
@observable includeArchived: boolean = false;
@observable pinToTop: boolean = !!this.props.match.params.query;
componentDidMount() {
@ -114,6 +90,11 @@ class Search extends React.Component<Props> {
this.fetchResultsDebounced();
};
handleFilterChange = ev => {
this.includeArchived = ev.target.checked;
this.fetchResultsDebounced();
};
@action
loadMoreResults = async () => {
// Don't paginate if there aren't more results or were in the middle of fetching
@ -132,6 +113,7 @@ class Search extends React.Component<Props> {
const results = await this.props.documents.search(this.query, {
offset: this.offset,
limit: DEFAULT_PAGINATION_LIMIT,
includeArchived: this.includeArchived,
});
if (results.length > 0) this.pinToTop = true;
@ -199,6 +181,17 @@ class Search extends React.Component<Props> {
</HelpText>
</Fade>
)}
{this.pinToTop && (
<Filters>
<Checkbox
label="Include archived"
name="includeArchived"
checked={this.includeArchived}
onChange={this.handleFilterChange}
small
/>
</Filters>
)}
{showEmpty && <Empty>No matching documents.</Empty>}
<ResultList column visible={this.pinToTop}>
<StyledArrowKeyNavigation
@ -231,4 +224,36 @@ class Search extends React.Component<Props> {
}
}
const Container = styled(CenteredContent)`
> div {
position: relative;
height: 100%;
}
`;
const ResultsWrapper = styled(Flex)`
position: absolute;
transition: all 300ms cubic-bezier(0.65, 0.05, 0.36, 1);
top: ${props => (props.pinToTop ? '0%' : '50%')};
margin-top: ${props => (props.pinToTop ? '40px' : '-75px')};
width: 100%;
`;
const ResultList = styled(Flex)`
margin-bottom: 150px;
opacity: ${props => (props.visible ? '1' : '0')};
transition: all 400ms cubic-bezier(0.65, 0.05, 0.36, 1);
`;
const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)`
display: flex;
flex-direction: column;
flex: 1;
`;
const Filters = styled(Flex)`
border-bottom: 1px solid ${props => props.theme.divider};
margin-bottom: 10px;
`;
export default withRouter(inject('documents')(Search));

View File

@ -367,12 +367,13 @@ router.post('documents.restore', auth(), async ctx => {
});
router.post('documents.search', auth(), pagination(), async ctx => {
const { query } = ctx.body;
const { query, includeArchived } = ctx.body;
const { offset, limit } = ctx.state.pagination;
ctx.assertPresent(query, 'query is required');
const user = ctx.state.user;
const results = await Document.searchForUser(user, query, {
includeArchived: includeArchived === 'true',
offset,
limit,
});

View File

@ -464,6 +464,29 @@ describe('#documents.search', async () => {
expect(body.data.length).toEqual(0);
});
it('should return archived documents if chosen', async () => {
const { user } = await seed();
const document = await buildDocument({
title: 'search term',
text: 'search term',
teamId: user.teamId,
});
await document.archive(user.id);
const res = await server.post('/api/documents.search', {
body: {
token: user.getJwtToken(),
query: 'search term',
includeArchived: 'true',
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.length).toEqual(1);
expect(body.data[0].document.text).toEqual('search term');
});
it('should not return documents in private collections not a member of', async () => {
const { user } = await seed();
const collection = await buildCollection({ private: true });

View File

@ -224,7 +224,7 @@ Document.searchForUser = async (
FROM documents
WHERE "searchVector" @@ to_tsquery('english', :query) AND
"collectionId" IN(:collectionIds) AND
"archivedAt" IS NULL AND
${options.includeArchived ? '' : '"archivedAt" IS NULL AND'}
"deletedAt" IS NULL AND
("publishedAt" IS NOT NULL OR "createdById" = '${user.id}')
ORDER BY

View File

@ -250,11 +250,13 @@ export default function Pricing() {
<Method method="documents.search" label="Search documents">
<Description>
This methods allows you to search all of your documents with
keywords.
This methods allows you to search your teams documents with
keywords. Search results will be restricted to those accessible by
the current access token.
</Description>
<Arguments>
<Argument id="query" description="Search query" required />
<Argument id="includeArchived" description="Boolean" />
</Arguments>
</Method>