parent
57e051d62b
commit
c1256c61aa
|
@ -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>
|
||||
|
|
|
@ -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 we’re 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));
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
Reference in New Issue