feat: Show recent searches (#2868)
* stash * root hookup * recent searches UI * feat: Add search query deletion * simplify no results state * lint
This commit is contained in:
28
app/models/SearchQuery.ts
Normal file
28
app/models/SearchQuery.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { client } from "~/utils/ApiClient";
|
||||
import BaseModel from "./BaseModel";
|
||||
|
||||
class SearchQuery extends BaseModel {
|
||||
id: string;
|
||||
|
||||
query: string;
|
||||
|
||||
delete = async () => {
|
||||
this.isSaving = true;
|
||||
|
||||
try {
|
||||
await client.post(`/searches.delete`, {
|
||||
query: this.query,
|
||||
});
|
||||
|
||||
this.store.data.forEach((searchQuery: SearchQuery) => {
|
||||
if (searchQuery.query === this.query) {
|
||||
this.store.remove(searchQuery.id);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default SearchQuery;
|
||||
@@ -2,19 +2,17 @@ import ArrowKeyNavigation from "boundless-arrow-key-navigation";
|
||||
import { isEqual } from "lodash";
|
||||
import { observable, action } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import queryString from "query-string";
|
||||
import * as React from "react";
|
||||
import { WithTranslation, withTranslation, Trans } from "react-i18next";
|
||||
import { RouteComponentProps, StaticContext, withRouter } from "react-router";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Waypoint } from "react-waypoint";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import { DateFilter as TDateFilter } from "@shared/types";
|
||||
import { DEFAULT_PAGINATION_LIMIT } from "~/stores/BaseStore";
|
||||
import { SearchParams } from "~/stores/DocumentsStore";
|
||||
import RootStore from "~/stores/RootStore";
|
||||
import Button from "~/components/Button";
|
||||
import CenteredContent from "~/components/CenteredContent";
|
||||
import DocumentListItem from "~/components/DocumentListItem";
|
||||
import Empty from "~/components/Empty";
|
||||
@@ -25,11 +23,11 @@ import LoadingIndicator from "~/components/LoadingIndicator";
|
||||
import PageTitle from "~/components/PageTitle";
|
||||
import RegisterKeyDown from "~/components/RegisterKeyDown";
|
||||
import withStores from "~/components/withStores";
|
||||
import NewDocumentMenu from "~/menus/NewDocumentMenu";
|
||||
import { newDocumentPath, searchUrl } from "~/utils/routeHelpers";
|
||||
import { searchUrl } from "~/utils/routeHelpers";
|
||||
import { decodeURIComponentSafe } from "~/utils/urls";
|
||||
import CollectionFilter from "./components/CollectionFilter";
|
||||
import DateFilter from "./components/DateFilter";
|
||||
import RecentSearches from "./components/RecentSearches";
|
||||
import SearchInput from "./components/SearchInput";
|
||||
import StatusFilter from "./components/StatusFilter";
|
||||
import UserFilter from "./components/UserFilter";
|
||||
@@ -46,11 +44,11 @@ type Props = RouteComponentProps<
|
||||
|
||||
@observer
|
||||
class Search extends React.Component<Props> {
|
||||
firstDocument: React.Component<any> | null | undefined;
|
||||
firstDocument: HTMLAnchorElement | null | undefined;
|
||||
|
||||
lastQuery = "";
|
||||
|
||||
lastParams: Record<string, any>;
|
||||
lastParams: SearchParams;
|
||||
|
||||
@observable
|
||||
query: string = decodeURIComponentSafe(this.props.match.params.term || "");
|
||||
@@ -67,9 +65,6 @@ class Search extends React.Component<Props> {
|
||||
@observable
|
||||
isLoading = false;
|
||||
|
||||
@observable
|
||||
pinToTop = !!this.props.match.params.term;
|
||||
|
||||
componentDidMount() {
|
||||
this.handleTermChange();
|
||||
|
||||
@@ -151,12 +146,6 @@ class Search extends React.Component<Props> {
|
||||
});
|
||||
};
|
||||
|
||||
handleNewDoc = () => {
|
||||
if (this.collectionId) {
|
||||
this.props.history.push(newDocumentPath(this.collectionId));
|
||||
}
|
||||
};
|
||||
|
||||
get includeArchived() {
|
||||
return this.params.get("includeArchived") === "true";
|
||||
}
|
||||
@@ -196,6 +185,7 @@ class Search extends React.Component<Props> {
|
||||
loadMoreResults = async () => {
|
||||
// Don't paginate if there aren't more results or we’re in the middle of fetching
|
||||
if (!this.allowLoadMore || this.isLoading) return;
|
||||
|
||||
// Fetch more results
|
||||
await this.fetchResults();
|
||||
};
|
||||
@@ -225,7 +215,6 @@ class Search extends React.Component<Props> {
|
||||
|
||||
try {
|
||||
const results = await this.props.documents.search(this.query, params);
|
||||
this.pinToTop = true;
|
||||
|
||||
if (results.length === 0 || results.length < DEFAULT_PAGINATION_LIMIT) {
|
||||
this.allowLoadMore = false;
|
||||
@@ -239,7 +228,6 @@ class Search extends React.Component<Props> {
|
||||
this.isLoading = false;
|
||||
}
|
||||
} else {
|
||||
this.pinToTop = false;
|
||||
this.isLoading = false;
|
||||
this.lastQuery = this.query;
|
||||
}
|
||||
@@ -252,17 +240,14 @@ class Search extends React.Component<Props> {
|
||||
});
|
||||
};
|
||||
|
||||
setFirstDocumentRef = (ref: any) => {
|
||||
setFirstDocumentRef = (ref: HTMLAnchorElement | null) => {
|
||||
this.firstDocument = ref;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { documents, notFound, location, t, auth, policies } = this.props;
|
||||
const { documents, notFound, t } = this.props;
|
||||
const results = documents.searchResults(this.query);
|
||||
const showEmpty = !this.isLoading && this.query && results.length === 0;
|
||||
const showShortcutTip = !this.pinToTop && location.state?.fromMenu;
|
||||
const can = policies.abilities(auth.team?.id ? auth.team.id : "");
|
||||
const canCollection = policies.abilities(this.collectionId || "");
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@@ -277,28 +262,14 @@ class Search extends React.Component<Props> {
|
||||
</Empty>
|
||||
</div>
|
||||
)}
|
||||
<ResultsWrapper pinToTop={this.pinToTop} column auto>
|
||||
<ResultsWrapper column auto>
|
||||
<SearchInput
|
||||
placeholder={`${t("Search")}…`}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
defaultValue={this.query}
|
||||
/>
|
||||
{showShortcutTip && (
|
||||
<Fade>
|
||||
<HelpText small>
|
||||
<Trans
|
||||
defaults="Use the <em>{{ shortcut }}</em> shortcut to search from anywhere in your knowledge base"
|
||||
values={{
|
||||
shortcut: "/",
|
||||
}}
|
||||
components={{
|
||||
em: <strong />,
|
||||
}}
|
||||
/>
|
||||
</HelpText>
|
||||
</Fade>
|
||||
)}
|
||||
{this.pinToTop && (
|
||||
|
||||
{this.query ? (
|
||||
<Filters>
|
||||
<StatusFilter
|
||||
includeArchived={this.includeArchived}
|
||||
@@ -333,39 +304,19 @@ class Search extends React.Component<Props> {
|
||||
}
|
||||
/>
|
||||
</Filters>
|
||||
) : (
|
||||
<RecentSearches />
|
||||
)}
|
||||
{showEmpty && (
|
||||
<Fade>
|
||||
<Centered column>
|
||||
<HelpText>
|
||||
<Trans>
|
||||
No documents found for your search filters. <br />
|
||||
</Trans>
|
||||
{can.createDocument && <Trans>Create a new document?</Trans>}
|
||||
<Trans>No documents found for your search filters.</Trans>
|
||||
</HelpText>
|
||||
<Wrapper>
|
||||
{this.collectionId &&
|
||||
can.createDocument &&
|
||||
canCollection.update ? (
|
||||
<Button
|
||||
onClick={this.handleNewDoc}
|
||||
icon={<PlusIcon />}
|
||||
primary
|
||||
>
|
||||
{t("New doc")}
|
||||
</Button>
|
||||
) : (
|
||||
<NewDocumentMenu />
|
||||
)}
|
||||
|
||||
<Button as={Link} to="/search" neutral>
|
||||
{t("Clear filters")}
|
||||
</Button>
|
||||
</Wrapper>
|
||||
</Centered>
|
||||
</Fade>
|
||||
)}
|
||||
<ResultList column visible={this.pinToTop}>
|
||||
<ResultList column>
|
||||
<StyledArrowKeyNavigation
|
||||
mode={ArrowKeyNavigation.mode.VERTICAL}
|
||||
defaultActiveChildIndex={0}
|
||||
@@ -396,11 +347,6 @@ class Search extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
const Wrapper = styled(Flex)`
|
||||
justify-content: center;
|
||||
margin: 10px 0;
|
||||
`;
|
||||
|
||||
const Centered = styled(Flex)`
|
||||
text-align: center;
|
||||
margin: 30vh auto 0;
|
||||
@@ -415,21 +361,14 @@ const Container = styled(CenteredContent)`
|
||||
}
|
||||
`;
|
||||
|
||||
const ResultsWrapper = styled(Flex)<{ pinToTop: boolean }>`
|
||||
position: absolute;
|
||||
transition: all 300ms cubic-bezier(0.65, 0.05, 0.36, 1);
|
||||
top: ${(props) => (props.pinToTop ? "0%" : "50%")};
|
||||
width: 100%;
|
||||
|
||||
const ResultsWrapper = styled(Flex)`
|
||||
${breakpoint("tablet")`
|
||||
margin-top: ${(props: any) => (props.pinToTop ? "40px" : "-75px")};
|
||||
margin-top: 40px;
|
||||
`};
|
||||
`;
|
||||
|
||||
const ResultList = styled(Flex)<{ visible: boolean }>`
|
||||
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)`
|
||||
|
||||
102
app/scenes/Search/components/RecentSearches.tsx
Normal file
102
app/scenes/Search/components/RecentSearches.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { CloseIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import Tooltip from "~/components/Tooltip";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { searchUrl } from "~/utils/routeHelpers";
|
||||
|
||||
function RecentSearches() {
|
||||
const { searches } = useStores();
|
||||
const { t } = useTranslation();
|
||||
|
||||
React.useEffect(() => {
|
||||
searches.fetchPage({});
|
||||
}, [searches]);
|
||||
|
||||
return searches.recent.length ? (
|
||||
<>
|
||||
<Heading>{t("Recent searches")}</Heading>
|
||||
<List>
|
||||
{searches.recent.map((searchQuery) => (
|
||||
<ListItem>
|
||||
<RecentSearch
|
||||
key={searchQuery.id}
|
||||
to={searchUrl(searchQuery.query)}
|
||||
>
|
||||
{searchQuery.query}
|
||||
<Tooltip tooltip={t("Remove search")} delay={150}>
|
||||
<RemoveButton
|
||||
onClick={(ev) => {
|
||||
ev.preventDefault();
|
||||
searchQuery.delete();
|
||||
}}
|
||||
>
|
||||
<CloseIcon color="currentColor" />
|
||||
</RemoveButton>
|
||||
</Tooltip>
|
||||
</RecentSearch>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</>
|
||||
) : null;
|
||||
}
|
||||
|
||||
const Heading = styled.h2`
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: ${(props) => props.theme.textSecondary};
|
||||
margin-bottom: 0;
|
||||
`;
|
||||
|
||||
const List = styled.ol`
|
||||
padding: 0;
|
||||
margin-top: 8px;
|
||||
`;
|
||||
|
||||
const ListItem = styled.li`
|
||||
font-size: 14px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: "·";
|
||||
color: ${(props) => props.theme.textTertiary};
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
}
|
||||
`;
|
||||
|
||||
const RemoveButton = styled(NudeButton)`
|
||||
opacity: 0;
|
||||
color: ${(props) => props.theme.textTertiary};
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
`;
|
||||
|
||||
const RecentSearch = styled(Link)`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: ${(props) => props.theme.textSecondary};
|
||||
padding: 1px 4px;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.text};
|
||||
background: ${(props) => props.theme.secondaryBackground};
|
||||
|
||||
${RemoveButton} {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default observer(RecentSearches);
|
||||
@@ -50,6 +50,8 @@ export default class BaseStore<T extends BaseModel> {
|
||||
|
||||
modelName: string;
|
||||
|
||||
apiEndpoint: string;
|
||||
|
||||
rootStore: RootStore;
|
||||
|
||||
actions = [
|
||||
@@ -65,6 +67,10 @@ export default class BaseStore<T extends BaseModel> {
|
||||
this.rootStore = rootStore;
|
||||
this.model = model;
|
||||
this.modelName = modelNameFromClassName(model.name);
|
||||
|
||||
if (!this.apiEndpoint) {
|
||||
this.apiEndpoint = `${this.modelName}s`;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -125,7 +131,7 @@ export default class BaseStore<T extends BaseModel> {
|
||||
this.isSaving = true;
|
||||
|
||||
try {
|
||||
const res = await client.post(`/${this.modelName}s.create`, {
|
||||
const res = await client.post(`/${this.apiEndpoint}.create`, {
|
||||
...params,
|
||||
...options,
|
||||
});
|
||||
@@ -150,7 +156,7 @@ export default class BaseStore<T extends BaseModel> {
|
||||
this.isSaving = true;
|
||||
|
||||
try {
|
||||
const res = await client.post(`/${this.modelName}s.update`, {
|
||||
const res = await client.post(`/${this.apiEndpoint}.update`, {
|
||||
...params,
|
||||
...options,
|
||||
});
|
||||
@@ -172,7 +178,7 @@ export default class BaseStore<T extends BaseModel> {
|
||||
this.isSaving = true;
|
||||
|
||||
try {
|
||||
await client.post(`/${this.modelName}s.delete`, {
|
||||
await client.post(`/${this.apiEndpoint}.delete`, {
|
||||
id: item.id,
|
||||
...options,
|
||||
});
|
||||
@@ -193,7 +199,7 @@ export default class BaseStore<T extends BaseModel> {
|
||||
this.isFetching = true;
|
||||
|
||||
try {
|
||||
const res = await client.post(`/${this.modelName}s.info`, {
|
||||
const res = await client.post(`/${this.apiEndpoint}.info`, {
|
||||
id,
|
||||
});
|
||||
invariant(res && res.data, "Data should be available");
|
||||
@@ -219,7 +225,7 @@ export default class BaseStore<T extends BaseModel> {
|
||||
this.isFetching = true;
|
||||
|
||||
try {
|
||||
const res = await client.post(`/${this.modelName}s.list`, params);
|
||||
const res = await client.post(`/${this.apiEndpoint}.list`, params);
|
||||
invariant(res && res.data, "Data not available");
|
||||
|
||||
runInAction(`list#${this.modelName}`, () => {
|
||||
|
||||
@@ -22,6 +22,16 @@ type FetchParams = PaginationParams & { collectionId: string };
|
||||
|
||||
type FetchPageParams = PaginationParams & { template?: boolean };
|
||||
|
||||
export type SearchParams = {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
dateFilter?: DateFilter;
|
||||
includeArchived?: boolean;
|
||||
includeDrafts?: boolean;
|
||||
collectionId?: string;
|
||||
userId?: string;
|
||||
};
|
||||
|
||||
type ImportOptions = {
|
||||
publish?: boolean;
|
||||
};
|
||||
@@ -392,15 +402,7 @@ export default class DocumentsStore extends BaseStore<Document> {
|
||||
@action
|
||||
search = async (
|
||||
query: string,
|
||||
options: {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
dateFilter?: DateFilter;
|
||||
includeArchived?: boolean;
|
||||
includeDrafts?: boolean;
|
||||
collectionId?: string;
|
||||
userId?: string;
|
||||
}
|
||||
options: SearchParams
|
||||
): Promise<SearchResult[]> => {
|
||||
const compactedOptions = omitBy(options, (o) => !o);
|
||||
const res = await client.get("/documents.search", {
|
||||
|
||||
@@ -14,6 +14,7 @@ import MembershipsStore from "./MembershipsStore";
|
||||
import NotificationSettingsStore from "./NotificationSettingsStore";
|
||||
import PoliciesStore from "./PoliciesStore";
|
||||
import RevisionsStore from "./RevisionsStore";
|
||||
import SearchesStore from "./SearchesStore";
|
||||
import SharesStore from "./SharesStore";
|
||||
import ToastsStore from "./ToastsStore";
|
||||
import UiStore from "./UiStore";
|
||||
@@ -36,6 +37,7 @@ export default class RootStore {
|
||||
presence: DocumentPresenceStore;
|
||||
policies: PoliciesStore;
|
||||
revisions: RevisionsStore;
|
||||
searches: SearchesStore;
|
||||
shares: SharesStore;
|
||||
ui: UiStore;
|
||||
users: UsersStore;
|
||||
@@ -60,6 +62,7 @@ export default class RootStore {
|
||||
this.notificationSettings = new NotificationSettingsStore(this);
|
||||
this.presence = new DocumentPresenceStore();
|
||||
this.revisions = new RevisionsStore(this);
|
||||
this.searches = new SearchesStore(this);
|
||||
this.shares = new SharesStore(this);
|
||||
this.ui = new UiStore();
|
||||
this.users = new UsersStore(this);
|
||||
@@ -83,6 +86,7 @@ export default class RootStore {
|
||||
this.presence.clear();
|
||||
this.policies.clear();
|
||||
this.revisions.clear();
|
||||
this.searches.clear();
|
||||
this.shares.clear();
|
||||
this.fileOperations.clear();
|
||||
// this.ui omitted to keep ui settings between sessions
|
||||
|
||||
20
app/stores/SearchesStore.ts
Normal file
20
app/stores/SearchesStore.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { uniqBy } from "lodash";
|
||||
import { computed } from "mobx";
|
||||
import SearchQuery from "~/models/SearchQuery";
|
||||
import BaseStore, { RPCAction } from "./BaseStore";
|
||||
import RootStore from "./RootStore";
|
||||
|
||||
export default class SearchesStore extends BaseStore<SearchQuery> {
|
||||
actions = [RPCAction.List, RPCAction.Delete];
|
||||
|
||||
apiEndpoint = "searches";
|
||||
|
||||
constructor(rootStore: RootStore) {
|
||||
super(rootStore, SearchQuery);
|
||||
}
|
||||
|
||||
@computed
|
||||
get recent(): SearchQuery[] {
|
||||
return uniqBy(this.orderedData, "query").slice(0, 8);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import "./collection";
|
||||
import "./document";
|
||||
import "./integration";
|
||||
import "./notificationSetting";
|
||||
import "./searchQuery";
|
||||
import "./share";
|
||||
import "./user";
|
||||
import "./team";
|
||||
|
||||
8
server/policies/searchQuery.ts
Normal file
8
server/policies/searchQuery.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { SearchQuery, User } from "@server/models";
|
||||
import policy from "./policy";
|
||||
|
||||
const { allow } = policy;
|
||||
|
||||
allow(User, ["read", "delete"], SearchQuery, (user, searchQuery) => {
|
||||
return user && user.id === searchQuery.userId;
|
||||
});
|
||||
@@ -12,6 +12,7 @@ import presentMembership from "./membership";
|
||||
import presentNotificationSetting from "./notificationSetting";
|
||||
import presentPolicies from "./policy";
|
||||
import presentRevision from "./revision";
|
||||
import presentSearchQuery from "./searchQuery";
|
||||
import presentShare from "./share";
|
||||
import presentSlackAttachment from "./slackAttachment";
|
||||
import presentTeam from "./team";
|
||||
@@ -29,6 +30,7 @@ export {
|
||||
presentRevision,
|
||||
presentCollection,
|
||||
presentShare,
|
||||
presentSearchQuery,
|
||||
presentTeam,
|
||||
presentGroup,
|
||||
presentIntegration,
|
||||
|
||||
7
server/presenters/searchQuery.ts
Normal file
7
server/presenters/searchQuery.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function present(searchQuery: any) {
|
||||
return {
|
||||
id: searchQuery.id,
|
||||
query: searchQuery.query,
|
||||
createdAt: searchQuery.createdAt,
|
||||
};
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import apiWrapper from "./middlewares/apiWrapper";
|
||||
import editor from "./middlewares/editor";
|
||||
import notificationSettings from "./notificationSettings";
|
||||
import revisions from "./revisions";
|
||||
import searches from "./searches";
|
||||
import shares from "./shares";
|
||||
import team from "./team";
|
||||
import users from "./users";
|
||||
@@ -53,6 +54,7 @@ router.use("/", revisions.routes());
|
||||
router.use("/", views.routes());
|
||||
router.use("/", hooks.routes());
|
||||
router.use("/", apiKeys.routes());
|
||||
router.use("/", searches.routes());
|
||||
router.use("/", shares.routes());
|
||||
router.use("/", team.routes());
|
||||
router.use("/", integrations.routes());
|
||||
|
||||
45
server/routes/api/searches.ts
Normal file
45
server/routes/api/searches.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import Router from "koa-router";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { SearchQuery } from "@server/models";
|
||||
import { presentSearchQuery } from "@server/presenters";
|
||||
import { assertPresent } from "@server/validation";
|
||||
import pagination from "./middlewares/pagination";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.post("searches.list", auth(), pagination(), async (ctx) => {
|
||||
const user = ctx.state.user;
|
||||
|
||||
const searches = await SearchQuery.findAll({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
order: [["createdAt", "DESC"]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data: searches.map(presentSearchQuery),
|
||||
};
|
||||
});
|
||||
|
||||
router.post("searches.delete", auth(), async (ctx) => {
|
||||
const { id, query } = ctx.body;
|
||||
assertPresent(id || query, "id or query is required");
|
||||
|
||||
const { user } = ctx.state;
|
||||
await SearchQuery.destroy({
|
||||
where: {
|
||||
...(id ? { id } : { query }),
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
};
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -11,7 +11,7 @@ import pagination from "./middlewares/pagination";
|
||||
const Op = Sequelize.Op;
|
||||
const { authorize } = policy;
|
||||
const router = new Router();
|
||||
// @ts-expect-error ts-migrate(7030) FIXME: Not all code paths return a value.
|
||||
|
||||
router.post("shares.info", auth(), async (ctx) => {
|
||||
const { id, documentId, apiVersion } = ctx.body;
|
||||
assertUuid(id || documentId, "id or documentId is required");
|
||||
@@ -40,7 +40,8 @@ router.post("shares.info", auth(), async (ctx) => {
|
||||
// Deprecated API response returns just the share for the current documentId
|
||||
if (apiVersion !== 2) {
|
||||
if (!share || !share.document) {
|
||||
return (ctx.response.status = 204);
|
||||
ctx.response.status = 204;
|
||||
return;
|
||||
}
|
||||
|
||||
authorize(user, "read", share);
|
||||
@@ -86,7 +87,8 @@ router.post("shares.info", auth(), async (ctx) => {
|
||||
}
|
||||
|
||||
if (!shares.length) {
|
||||
return (ctx.response.status = 204);
|
||||
ctx.response.status = 204;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
|
||||
@@ -497,6 +497,8 @@
|
||||
"Past week": "Past week",
|
||||
"Past month": "Past month",
|
||||
"Past year": "Past year",
|
||||
"Recent searches": "Recent searches",
|
||||
"Remove search": "Remove search",
|
||||
"Active documents": "Active documents",
|
||||
"Documents in collections you are able to access": "Documents in collections you are able to access",
|
||||
"All documents": "All documents",
|
||||
@@ -505,10 +507,7 @@
|
||||
"Author": "Author",
|
||||
"Not Found": "Not Found",
|
||||
"We were unable to find the page you’re looking for.": "We were unable to find the page you’re looking for.",
|
||||
"Use the <em>{{ shortcut }}</em> shortcut to search from anywhere in your knowledge base": "Use the <em>{{ shortcut }}</em> shortcut to search from anywhere in your knowledge base",
|
||||
"No documents found for your search filters. <1></1>": "No documents found for your search filters. <1></1>",
|
||||
"Create a new document?": "Create a new document?",
|
||||
"Clear filters": "Clear filters",
|
||||
"No documents found for your search filters.": "No documents found for your search filters.",
|
||||
"Processing": "Processing",
|
||||
"Expired": "Expired",
|
||||
"Error": "Error",
|
||||
|
||||
Reference in New Issue
Block a user