feat: allow searching for urls of internal documents (#1529)
* core search logic * bump version of rich markdown editor * let shift and meta modifiers do their thing when clicking on a link in a doc * version bump editor * test: Add parseDocumentSlug test Co-authored-by: Tom Moor <tom.moor@gmail.com>
This commit is contained in:
@ -34,14 +34,14 @@ class Editor extends React.Component<PropsWithRef> {
|
|||||||
return result.url;
|
return result.url;
|
||||||
};
|
};
|
||||||
|
|
||||||
onClickLink = (href: string) => {
|
onClickLink = (href: string, event: MouseEvent) => {
|
||||||
// on page hash
|
// on page hash
|
||||||
if (href[0] === "#") {
|
if (href[0] === "#") {
|
||||||
window.location.href = href;
|
window.location.href = href;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInternalUrl(href)) {
|
if (isInternalUrl(href) && !event.metaKey && !event.shiftKey) {
|
||||||
// relative
|
// relative
|
||||||
let navigateTo = href;
|
let navigateTo = href;
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import * as React from "react";
|
|||||||
import { Portal } from "react-portal";
|
import { Portal } from "react-portal";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { fadeAndSlideIn } from "shared/styles/animations";
|
import { fadeAndSlideIn } from "shared/styles/animations";
|
||||||
import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentSlug";
|
import parseDocumentSlug from "shared/utils/parseDocumentSlug";
|
||||||
import DocumentsStore from "stores/DocumentsStore";
|
import DocumentsStore from "stores/DocumentsStore";
|
||||||
import HoverPreviewDocument from "components/HoverPreviewDocument";
|
import HoverPreviewDocument from "components/HoverPreviewDocument";
|
||||||
import isInternalUrl from "utils/isInternalUrl";
|
import isInternalUrl from "utils/isInternalUrl";
|
||||||
@ -21,7 +21,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function HoverPreviewInternal({ node, documents, onClose, event }: Props) {
|
function HoverPreviewInternal({ node, documents, onClose, event }: Props) {
|
||||||
const slug = parseDocumentSlugFromUrl(node.href);
|
const slug = parseDocumentSlug(node.href);
|
||||||
|
|
||||||
const [isVisible, setVisible] = React.useState(false);
|
const [isVisible, setVisible] = React.useState(false);
|
||||||
const timerClose = React.useRef();
|
const timerClose = React.useRef();
|
||||||
|
@ -3,7 +3,7 @@ import { inject, observer } from "mobx-react";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentSlug";
|
import parseDocumentSlug from "shared/utils/parseDocumentSlug";
|
||||||
import DocumentsStore from "stores/DocumentsStore";
|
import DocumentsStore from "stores/DocumentsStore";
|
||||||
import DocumentMetaWithViews from "components/DocumentMetaWithViews";
|
import DocumentMetaWithViews from "components/DocumentMetaWithViews";
|
||||||
import Editor from "components/Editor";
|
import Editor from "components/Editor";
|
||||||
@ -15,7 +15,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function HoverPreviewDocument({ url, documents, children }: Props) {
|
function HoverPreviewDocument({ url, documents, children }: Props) {
|
||||||
const slug = parseDocumentSlugFromUrl(url);
|
const slug = parseDocumentSlug(url);
|
||||||
|
|
||||||
documents.prefetchDocument(slug, {
|
documents.prefetchDocument(slug, {
|
||||||
prefetch: true,
|
prefetch: true,
|
||||||
|
@ -5,6 +5,7 @@ import { observer, inject } from "mobx-react";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import type { RouterHistory, Match } from "react-router-dom";
|
import type { RouterHistory, Match } from "react-router-dom";
|
||||||
import { withRouter } from "react-router-dom";
|
import { withRouter } from "react-router-dom";
|
||||||
|
import parseDocumentSlug from "shared/utils/parseDocumentSlug";
|
||||||
import DocumentsStore from "stores/DocumentsStore";
|
import DocumentsStore from "stores/DocumentsStore";
|
||||||
import PoliciesStore from "stores/PoliciesStore";
|
import PoliciesStore from "stores/PoliciesStore";
|
||||||
import RevisionsStore from "stores/RevisionsStore";
|
import RevisionsStore from "stores/RevisionsStore";
|
||||||
@ -20,6 +21,7 @@ import Loading from "./Loading";
|
|||||||
import SocketPresence from "./SocketPresence";
|
import SocketPresence from "./SocketPresence";
|
||||||
import { type LocationWithState } from "types";
|
import { type LocationWithState } from "types";
|
||||||
import { NotFoundError, OfflineError } from "utils/errors";
|
import { NotFoundError, OfflineError } from "utils/errors";
|
||||||
|
import isInternalUrl from "utils/isInternalUrl";
|
||||||
import { matchDocumentEdit, updateDocumentUrl } from "utils/routeHelpers";
|
import { matchDocumentEdit, updateDocumentUrl } from "utils/routeHelpers";
|
||||||
|
|
||||||
type Props = {|
|
type Props = {|
|
||||||
@ -70,6 +72,26 @@ class DataLoader extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSearchLink = async (term: string) => {
|
onSearchLink = async (term: string) => {
|
||||||
|
if (isInternalUrl(term)) {
|
||||||
|
// search for exact internal document
|
||||||
|
const slug = parseDocumentSlug(term);
|
||||||
|
try {
|
||||||
|
const document = await this.props.documents.fetch(slug);
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: document.title,
|
||||||
|
url: document.url,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} catch (error) {
|
||||||
|
// NotFoundError could not find document for slug
|
||||||
|
if (!(error instanceof NotFoundError)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.search(term);
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
@ -138,7 +138,7 @@
|
|||||||
"react-portal": "^4.0.0",
|
"react-portal": "^4.0.0",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-waypoint": "^9.0.2",
|
"react-waypoint": "^9.0.2",
|
||||||
"rich-markdown-editor": "^11.0.0-1",
|
"rich-markdown-editor": "^11.0.0-4",
|
||||||
"semver": "^7.3.2",
|
"semver": "^7.3.2",
|
||||||
"sequelize": "^6.3.4",
|
"sequelize": "^6.3.4",
|
||||||
"sequelize-cli": "^6.2.0",
|
"sequelize-cli": "^6.2.0",
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
export function parseDocumentSlugFromUrl(url: string) {
|
export default function parseDocumentSlug(url: string) {
|
||||||
let parsed;
|
let parsed;
|
||||||
try {
|
if (url[0] === "/") {
|
||||||
parsed = new URL(url);
|
parsed = url;
|
||||||
} catch (err) {
|
} else {
|
||||||
return;
|
try {
|
||||||
|
parsed = new URL(url).pathname;
|
||||||
|
} catch (err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsed.pathname.replace(/^\/doc\//, "");
|
return parsed.replace(/^\/doc\//, "");
|
||||||
}
|
}
|
||||||
|
22
shared/utils/parseDocumentSlug.test.js
Normal file
22
shared/utils/parseDocumentSlug.test.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// @flow
|
||||||
|
import parseDocumentSlug from "./parseDocumentSlug";
|
||||||
|
|
||||||
|
describe("#parseDocumentSlug", () => {
|
||||||
|
it("should work with fully qualified url", () => {
|
||||||
|
expect(
|
||||||
|
parseDocumentSlug("http://example.com/doc/my-doc-y4j4tR4UuV")
|
||||||
|
).toEqual("my-doc-y4j4tR4UuV");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with subdomain qualified url", () => {
|
||||||
|
expect(
|
||||||
|
parseDocumentSlug("http://mywiki.getoutline.com/doc/my-doc-y4j4tR4UuV")
|
||||||
|
).toEqual("my-doc-y4j4tR4UuV");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with path", () => {
|
||||||
|
expect(parseDocumentSlug("/doc/my-doc-y4j4tR4UuV")).toEqual(
|
||||||
|
"my-doc-y4j4tR4UuV"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -9806,10 +9806,10 @@ retry-as-promised@^3.2.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
any-promise "^1.3.0"
|
any-promise "^1.3.0"
|
||||||
|
|
||||||
rich-markdown-editor@^11.0.0-1:
|
rich-markdown-editor@^11.0.0-4:
|
||||||
version "11.0.0-1"
|
version "11.0.0-4"
|
||||||
resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-11.0.0-1.tgz#d21f0a62153bf8b94dc82652f5c1cbbd861eb5c8"
|
resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-11.0.0-4.tgz#b65f5b03502d70a2b2bbea5c916c23b071f4bab6"
|
||||||
integrity sha512-XUhsojdDsfKzOMLmIE5E8Pl6lNYWjlHqBpnDEa21oFZE0byFNoDTK84dcJg4ErNrk8OZqFuDjZVBeglgIzrtnQ==
|
integrity sha512-+llzd8Plxzsc/jJ8RwtMSV5QIpxpZdM5nQejG/SLe/lfqHNOFNnIiOszSPERIcULLxsLdMT5Ajz+Yr5PXPicOQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
copy-to-clipboard "^3.0.8"
|
copy-to-clipboard "^3.0.8"
|
||||||
lodash "^4.17.11"
|
lodash "^4.17.11"
|
||||||
|
Reference in New Issue
Block a user