@ -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>
|
||||||
|
@ -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 we’re in the middle of fetching
|
// 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, {
|
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));
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
@ -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 });
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user