feat: Improved search results when finding links in document editor (#1573)

* feat: Improved search results when finding links in document editor

* chore(deps): Bump RME for smoother link search
This commit is contained in:
Tom Moor 2020-09-26 11:07:49 -07:00 committed by GitHub
parent 6f1f855083
commit f1a95e5e79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 131 additions and 15 deletions

View File

@ -1,6 +1,7 @@
// @flow
import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
import invariant from "invariant";
import { deburr, sortBy } from "lodash";
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import * as React from "react";
@ -97,20 +98,26 @@ class DataLoader extends React.Component<Props> {
}
// default search for anything that doesn't look like a URL
const results = await this.props.documents.search(term);
const results = await this.props.documents.searchTitles(term);
return results
.filter((result) => result.document.title)
.map((result) => {
const time = distanceInWordsToNow(result.document.updatedAt, {
return sortBy(
results.map((document) => {
const time = distanceInWordsToNow(document.updatedAt, {
addSuffix: true,
});
return {
title: result.document.title,
title: document.title,
subtitle: `Updated ${time}`,
url: result.document.url,
url: document.url,
};
});
}),
(document) =>
deburr(document.title)
.toLowerCase()
.startsWith(deburr(term).toLowerCase())
? -1
: 1
);
};
onCreateLink = async (title: string) => {

View File

@ -312,12 +312,25 @@ export default class DocumentsStore extends BaseStore<Document> {
return this.fetchNamedPage("list", options);
};
@action
searchTitles = async (query: string, options: PaginationParams = {}) => {
const res = await client.get("/documents.search_titles", {
query,
...options,
});
invariant(res && res.data, "Search response should be available");
// add the documents and associated policies to the store
res.data.forEach(this.add);
this.addPolicies(res.policies);
return res.data;
};
@action
search = async (
query: string,
options: PaginationParams = {}
): Promise<SearchResult[]> => {
// $FlowFixMe
const compactedOptions = omitBy(options, (o) => !o);
const res = await client.get("/documents.search", {
...compactedOptions,
@ -464,7 +477,7 @@ export default class DocumentsStore extends BaseStore<Document> {
{ key: "title", value: title },
{ key: "publish", value: options.publish },
{ key: "file", value: file },
].map((info) => {
].forEach((info) => {
if (typeof info.value === "string" && info.value) {
formData.append(info.key, info.value);
}

View File

@ -140,7 +140,7 @@
"react-portal": "^4.0.0",
"react-router-dom": "^5.1.2",
"react-waypoint": "^9.0.2",
"rich-markdown-editor": "^11.0.0-8",
"rich-markdown-editor": "^11.0.0-9",
"semver": "^7.3.2",
"sequelize": "^6.3.4",
"sequelize-cli": "^6.2.0",

View File

@ -551,6 +551,52 @@ router.post("documents.restore", auth(), async (ctx) => {
};
});
router.post("documents.search_titles", auth(), pagination(), async (ctx) => {
const { query } = ctx.body;
const { offset, limit } = ctx.state.pagination;
const user = ctx.state.user;
ctx.assertPresent(query, "query is required");
const collectionIds = await user.collectionIds();
const documents = await Document.scope(
{
method: ["withViews", user.id],
},
{
method: ["withCollection", user.id],
}
).findAll({
where: {
title: {
[Op.iLike]: `%${query}%`,
},
collectionId: collectionIds,
archivedAt: {
[Op.eq]: null,
},
},
order: [["updatedAt", "DESC"]],
include: [
{ model: User, as: "createdBy", paranoid: false },
{ model: User, as: "updatedBy", paranoid: false },
],
offset,
limit,
});
const policies = presentPolicies(user, documents);
const data = await Promise.all(
documents.map((document) => presentDocument(document))
);
ctx.body = {
pagination: ctx.state.pagination,
data,
policies,
};
});
router.post("documents.search", auth(), pagination(), async (ctx) => {
const {
query,

View File

@ -628,6 +628,56 @@ describe("#documents.drafts", () => {
});
});
describe("#documents.search_titles", () => {
it("should return case insensitive results for partial query", async () => {
const user = await buildUser();
const document = await buildDocument({
userId: user.id,
teamId: user.teamId,
title: "Super secret",
});
const res = await server.post("/api/documents.search_titles", {
body: { token: user.getJwtToken(), query: "SECRET" },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.length).toEqual(1);
expect(body.data[0].id).toEqual(document.id);
});
it("should not include archived or deleted documents", async () => {
const user = await buildUser();
await buildDocument({
userId: user.id,
teamId: user.teamId,
title: "Super secret",
archivedAt: new Date(),
});
await buildDocument({
userId: user.id,
teamId: user.teamId,
title: "Super secret",
deletedAt: new Date(),
});
const res = await server.post("/api/documents.search_titles", {
body: { token: user.getJwtToken(), query: "SECRET" },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.length).toEqual(0);
});
it("should require authentication", async () => {
const res = await server.post("/api/documents.search_titles");
expect(res.status).toEqual(401);
});
});
describe("#documents.search", () => {
it("should return results", async () => {
const { user } = await seed();

View File

@ -9887,10 +9887,10 @@ retry-as-promised@^3.2.0:
dependencies:
any-promise "^1.3.0"
rich-markdown-editor@^11.0.0-8:
version "11.0.0-8"
resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-11.0.0-8.tgz#542a33963147077a3c01ba2f689f67714056bbb9"
integrity sha512-TTMVMr33CFlk+AkHKE2/nOZ4VRSwq0aOLodCYotztSwGAQ1a01znZmue1gn/VbIrF7/AtxZ/cnIs3p2hJr82bw==
rich-markdown-editor@^11.0.0-9:
version "11.0.0-9"
resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-11.0.0-9.tgz#a7a4bfa09fca3cdf3168027e11fd9af46c708680"
integrity sha512-B1q6VbRF/6yjHsYMQEXjgjwPJCSU3mNEmLGsJOF+PZACII5ojg8bV51jGd4W1rTvbIzqnLK4iPWlAbn+hrMtXw==
dependencies:
copy-to-clipboard "^3.0.8"
lodash "^4.17.11"