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
6 changed files with 89 additions and 34 deletions

View File

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

View File

@ -18,6 +18,8 @@ import { meta } from 'utils/keyboard';
import Flex from 'shared/components/Flex'; import Flex from 'shared/components/Flex';
import Empty from 'components/Empty'; import Empty from 'components/Empty';
import Fade from 'components/Fade'; import Fade from 'components/Fade';
import Checkbox from 'components/Checkbox';
import HelpText from 'components/HelpText'; import HelpText from 'components/HelpText';
import CenteredContent from 'components/CenteredContent'; import CenteredContent from 'components/CenteredContent';
import LoadingIndicator from 'components/LoadingIndicator'; import LoadingIndicator from 'components/LoadingIndicator';
@ -33,33 +35,6 @@ type Props = {
notFound: ?boolean, 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 @observer
class Search extends React.Component<Props> { class Search extends React.Component<Props> {
firstDocument: ?DocumentPreview; firstDocument: ?DocumentPreview;
@ -68,6 +43,7 @@ class Search extends React.Component<Props> {
@observable offset: number = 0; @observable offset: number = 0;
@observable allowLoadMore: boolean = true; @observable allowLoadMore: boolean = true;
@observable isFetching: boolean = false; @observable isFetching: boolean = false;
@observable includeArchived: boolean = false;
@observable pinToTop: boolean = !!this.props.match.params.query; @observable pinToTop: boolean = !!this.props.match.params.query;
componentDidMount() { componentDidMount() {
@ -114,6 +90,11 @@ class Search extends React.Component<Props> {
this.fetchResultsDebounced(); this.fetchResultsDebounced();
}; };
handleFilterChange = ev => {
this.includeArchived = ev.target.checked;
this.fetchResultsDebounced();
};
@action @action
loadMoreResults = async () => { loadMoreResults = async () => {
// Don't paginate if there aren't more results or were in the middle of fetching // 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, { const results = await this.props.documents.search(this.query, {
offset: this.offset, offset: this.offset,
limit: DEFAULT_PAGINATION_LIMIT, limit: DEFAULT_PAGINATION_LIMIT,
includeArchived: this.includeArchived,
}); });
if (results.length > 0) this.pinToTop = true; if (results.length > 0) this.pinToTop = true;
@ -199,6 +181,17 @@ class Search extends React.Component<Props> {
</HelpText> </HelpText>
</Fade> </Fade>
)} )}
{this.pinToTop && (
<Filters>
<Checkbox
label="Include archived"
name="includeArchived"
checked={this.includeArchived}
onChange={this.handleFilterChange}
small
/>
</Filters>
)}
{showEmpty && <Empty>No matching documents.</Empty>} {showEmpty && <Empty>No matching documents.</Empty>}
<ResultList column visible={this.pinToTop}> <ResultList column visible={this.pinToTop}>
<StyledArrowKeyNavigation <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)); 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 => { router.post('documents.search', auth(), pagination(), async ctx => {
const { query } = ctx.body; const { query, includeArchived } = ctx.body;
const { offset, limit } = ctx.state.pagination; const { offset, limit } = ctx.state.pagination;
ctx.assertPresent(query, 'query is required'); ctx.assertPresent(query, 'query is required');
const user = ctx.state.user; const user = ctx.state.user;
const results = await Document.searchForUser(user, query, { const results = await Document.searchForUser(user, query, {
includeArchived: includeArchived === 'true',
offset, offset,
limit, limit,
}); });

View File

@ -464,6 +464,29 @@ describe('#documents.search', async () => {
expect(body.data.length).toEqual(0); 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 () => { it('should not return documents in private collections not a member of', async () => {
const { user } = await seed(); const { user } = await seed();
const collection = await buildCollection({ private: true }); const collection = await buildCollection({ private: true });

View File

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

View File

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