diff --git a/app/components/Editor.js b/app/components/Editor.js index cdba893a..73739604 100644 --- a/app/components/Editor.js +++ b/app/components/Editor.js @@ -34,14 +34,14 @@ class Editor extends React.Component { return result.url; }; - onClickLink = (href: string) => { + onClickLink = (href: string, event: MouseEvent) => { // on page hash if (href[0] === "#") { window.location.href = href; return; } - if (isInternalUrl(href)) { + if (isInternalUrl(href) && !event.metaKey && !event.shiftKey) { // relative let navigateTo = href; diff --git a/app/components/HoverPreview.js b/app/components/HoverPreview.js index 198f5eeb..62496e83 100644 --- a/app/components/HoverPreview.js +++ b/app/components/HoverPreview.js @@ -5,7 +5,7 @@ import * as React from "react"; import { Portal } from "react-portal"; import styled from "styled-components"; import { fadeAndSlideIn } from "shared/styles/animations"; -import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentSlug"; +import parseDocumentSlug from "shared/utils/parseDocumentSlug"; import DocumentsStore from "stores/DocumentsStore"; import HoverPreviewDocument from "components/HoverPreviewDocument"; import isInternalUrl from "utils/isInternalUrl"; @@ -21,7 +21,7 @@ type 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 timerClose = React.useRef(); diff --git a/app/components/HoverPreviewDocument.js b/app/components/HoverPreviewDocument.js index b5e9a2c7..54fc497a 100644 --- a/app/components/HoverPreviewDocument.js +++ b/app/components/HoverPreviewDocument.js @@ -3,7 +3,7 @@ import { inject, observer } from "mobx-react"; import * as React from "react"; import { Link } from "react-router-dom"; import styled from "styled-components"; -import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentSlug"; +import parseDocumentSlug from "shared/utils/parseDocumentSlug"; import DocumentsStore from "stores/DocumentsStore"; import DocumentMetaWithViews from "components/DocumentMetaWithViews"; import Editor from "components/Editor"; @@ -15,7 +15,7 @@ type Props = { }; function HoverPreviewDocument({ url, documents, children }: Props) { - const slug = parseDocumentSlugFromUrl(url); + const slug = parseDocumentSlug(url); documents.prefetchDocument(slug, { prefetch: true, diff --git a/app/scenes/Document/components/DataLoader.js b/app/scenes/Document/components/DataLoader.js index 9b2f2abd..5cc625b7 100644 --- a/app/scenes/Document/components/DataLoader.js +++ b/app/scenes/Document/components/DataLoader.js @@ -5,6 +5,7 @@ import { observer, inject } from "mobx-react"; import * as React from "react"; import type { RouterHistory, Match } from "react-router-dom"; import { withRouter } from "react-router-dom"; +import parseDocumentSlug from "shared/utils/parseDocumentSlug"; import DocumentsStore from "stores/DocumentsStore"; import PoliciesStore from "stores/PoliciesStore"; import RevisionsStore from "stores/RevisionsStore"; @@ -20,6 +21,7 @@ import Loading from "./Loading"; import SocketPresence from "./SocketPresence"; import { type LocationWithState } from "types"; import { NotFoundError, OfflineError } from "utils/errors"; +import isInternalUrl from "utils/isInternalUrl"; import { matchDocumentEdit, updateDocumentUrl } from "utils/routeHelpers"; type Props = {| @@ -70,6 +72,26 @@ class DataLoader extends React.Component { } 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); return results diff --git a/package.json b/package.json index 52d45f95..a61be4a8 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,7 @@ "react-portal": "^4.0.0", "react-router-dom": "^5.1.2", "react-waypoint": "^9.0.2", - "rich-markdown-editor": "^11.0.0-1", + "rich-markdown-editor": "^11.0.0-4", "semver": "^7.3.2", "sequelize": "^6.3.4", "sequelize-cli": "^6.2.0", diff --git a/shared/utils/parseDocumentSlug.js b/shared/utils/parseDocumentSlug.js index 19e9ed6b..7cc5b72f 100644 --- a/shared/utils/parseDocumentSlug.js +++ b/shared/utils/parseDocumentSlug.js @@ -1,12 +1,16 @@ // @flow -export function parseDocumentSlugFromUrl(url: string) { +export default function parseDocumentSlug(url: string) { let parsed; - try { - parsed = new URL(url); - } catch (err) { - return; + if (url[0] === "/") { + parsed = url; + } else { + try { + parsed = new URL(url).pathname; + } catch (err) { + return; + } } - return parsed.pathname.replace(/^\/doc\//, ""); + return parsed.replace(/^\/doc\//, ""); } diff --git a/shared/utils/parseDocumentSlug.test.js b/shared/utils/parseDocumentSlug.test.js new file mode 100644 index 00000000..918d55a1 --- /dev/null +++ b/shared/utils/parseDocumentSlug.test.js @@ -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" + ); + }); +}); diff --git a/yarn.lock b/yarn.lock index 04ad34c3..82275f95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9806,10 +9806,10 @@ retry-as-promised@^3.2.0: dependencies: any-promise "^1.3.0" -rich-markdown-editor@^11.0.0-1: - version "11.0.0-1" - resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-11.0.0-1.tgz#d21f0a62153bf8b94dc82652f5c1cbbd861eb5c8" - integrity sha512-XUhsojdDsfKzOMLmIE5E8Pl6lNYWjlHqBpnDEa21oFZE0byFNoDTK84dcJg4ErNrk8OZqFuDjZVBeglgIzrtnQ== +rich-markdown-editor@^11.0.0-4: + version "11.0.0-4" + resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-11.0.0-4.tgz#b65f5b03502d70a2b2bbea5c916c23b071f4bab6" + integrity sha512-+llzd8Plxzsc/jJ8RwtMSV5QIpxpZdM5nQejG/SLe/lfqHNOFNnIiOszSPERIcULLxsLdMT5Ajz+Yr5PXPicOQ== dependencies: copy-to-clipboard "^3.0.8" lodash "^4.17.11"