fix: Link preview and search should work on collection descriptions (#3355)

This commit is contained in:
Tom Moor
2022-04-09 19:00:56 -07:00
committed by GitHub
parent a47427de9e
commit 48fad5cfa0
4 changed files with 98 additions and 90 deletions

View File

@@ -1,15 +1,23 @@
import { formatDistanceToNow } from "date-fns";
import { deburr, sortBy } from "lodash";
import * as React from "react"; import * as React from "react";
import { Optional } from "utility-types"; import { Optional } from "utility-types";
import embeds from "@shared/editor/embeds"; import embeds from "@shared/editor/embeds";
import parseDocumentSlug from "@shared/utils/parseDocumentSlug";
import { isInternalUrl } from "@shared/utils/urls"; import { isInternalUrl } from "@shared/utils/urls";
import Document from "~/models/Document";
import ErrorBoundary from "~/components/ErrorBoundary"; import ErrorBoundary from "~/components/ErrorBoundary";
import HoverPreview from "~/components/HoverPreview";
import type { Props as EditorProps, Editor as SharedEditor } from "~/editor"; import type { Props as EditorProps, Editor as SharedEditor } from "~/editor";
import useDictionary from "~/hooks/useDictionary"; import useDictionary from "~/hooks/useDictionary";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts"; import useToasts from "~/hooks/useToasts";
import { NotFoundError } from "~/utils/errors";
import { uploadFile } from "~/utils/files"; import { uploadFile } from "~/utils/files";
import history from "~/utils/history"; import history from "~/utils/history";
import { isModKey } from "~/utils/keyboard"; import { isModKey } from "~/utils/keyboard";
import { isHash } from "~/utils/urls"; import { isHash } from "~/utils/urls";
import DocumentBreadcrumb from "./DocumentBreadcrumb";
const LazyLoadedEditor = React.lazy( const LazyLoadedEditor = React.lazy(
() => () =>
@@ -38,8 +46,74 @@ export type Props = Optional<
function Editor(props: Props, ref: React.Ref<SharedEditor>) { function Editor(props: Props, ref: React.Ref<SharedEditor>) {
const { id, shareId } = props; const { id, shareId } = props;
const { documents } = useStores();
const { showToast } = useToasts(); const { showToast } = useToasts();
const dictionary = useDictionary(); const dictionary = useDictionary();
const [
activeLinkEvent,
setActiveLinkEvent,
] = React.useState<MouseEvent | null>(null);
const handleLinkActive = React.useCallback((event: MouseEvent) => {
setActiveLinkEvent(event);
return false;
}, []);
const handleLinkInactive = React.useCallback(() => {
setActiveLinkEvent(null);
}, []);
const handleSearchLink = React.useCallback(
async (term: string) => {
if (isInternalUrl(term)) {
// search for exact internal document
const slug = parseDocumentSlug(term);
if (!slug) {
return [];
}
try {
const document = await documents.fetch(slug);
const time = formatDistanceToNow(Date.parse(document.updatedAt), {
addSuffix: true,
});
return [
{
title: document.title,
subtitle: `Updated ${time}`,
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 documents.searchTitles(term);
return sortBy(
results.map((document: Document) => {
return {
title: document.title,
subtitle: <DocumentBreadcrumb document={document} onlyText />,
url: document.url,
};
}),
(document) =>
deburr(document.title)
.toLowerCase()
.startsWith(deburr(term).toLowerCase())
? -1
: 1
);
},
[documents]
);
const onUploadFile = React.useCallback( const onUploadFile = React.useCallback(
async (file: File) => { async (file: File) => {
@@ -87,6 +161,7 @@ function Editor(props: Props, ref: React.Ref<SharedEditor>) {
return ( return (
<ErrorBoundary reloadOnChunkMissing> <ErrorBoundary reloadOnChunkMissing>
<>
<LazyLoadedEditor <LazyLoadedEditor
ref={ref} ref={ref}
uploadFile={onUploadFile} uploadFile={onUploadFile}
@@ -94,10 +169,20 @@ function Editor(props: Props, ref: React.Ref<SharedEditor>) {
embeds={embeds} embeds={embeds}
dictionary={dictionary} dictionary={dictionary}
{...props} {...props}
onHoverLink={handleLinkActive}
onClickLink={onClickLink} onClickLink={onClickLink}
onSearchLink={handleSearchLink}
placeholder={props.placeholder || ""} placeholder={props.placeholder || ""}
defaultValue={props.defaultValue || ""} defaultValue={props.defaultValue || ""}
/> />
{activeLinkEvent && !shareId && (
<HoverPreview
node={activeLinkEvent.target as HTMLAnchorElement}
event={activeLinkEvent}
onClose={handleLinkInactive}
/>
)}
</>
</ErrorBoundary> </ErrorBoundary>
); );
} }

View File

@@ -126,7 +126,7 @@ function HoverPreviewInternal({ node, onClose }: Props) {
function HoverPreview({ node, ...rest }: Props) { function HoverPreview({ node, ...rest }: Props) {
const isMobile = useMobile(); const isMobile = useMobile();
if (!isMobile) { if (isMobile) {
return null; return null;
} }
@@ -157,7 +157,7 @@ const Margin = styled.div`
const CardContent = styled.div` const CardContent = styled.div`
overflow: hidden; overflow: hidden;
max-height: 350px; max-height: 20em;
user-select: none; user-select: none;
`; `;

View File

@@ -1,18 +1,13 @@
import { formatDistanceToNow } from "date-fns";
import invariant from "invariant"; import invariant from "invariant";
import { deburr, sortBy } from "lodash";
import { observable } from "mobx"; import { observable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import { RouteComponentProps, StaticContext } from "react-router"; import { RouteComponentProps, StaticContext } from "react-router";
import parseDocumentSlug from "@shared/utils/parseDocumentSlug";
import { isInternalUrl } from "@shared/utils/urls";
import RootStore from "~/stores/RootStore"; import RootStore from "~/stores/RootStore";
import Document from "~/models/Document"; import Document from "~/models/Document";
import Revision from "~/models/Revision"; import Revision from "~/models/Revision";
import Error404 from "~/scenes/Error404"; import Error404 from "~/scenes/Error404";
import ErrorOffline from "~/scenes/ErrorOffline"; import ErrorOffline from "~/scenes/ErrorOffline";
import DocumentBreadcrumb from "~/components/DocumentBreadcrumb";
import withStores from "~/components/withStores"; import withStores from "~/components/withStores";
import { NavigationNode } from "~/types"; import { NavigationNode } from "~/types";
import { NotFoundError, OfflineError } from "~/utils/errors"; import { NotFoundError, OfflineError } from "~/utils/errors";
@@ -100,55 +95,6 @@ class DataLoader extends React.Component<Props> {
return this.isEditRoute || this.props.auth?.team?.collaborativeEditing; return this.isEditRoute || this.props.auth?.team?.collaborativeEditing;
} }
onSearchLink = async (term: string) => {
if (isInternalUrl(term)) {
// search for exact internal document
const slug = parseDocumentSlug(term);
if (!slug) {
return;
}
try {
const document = await this.props.documents.fetch(slug);
const time = formatDistanceToNow(Date.parse(document.updatedAt), {
addSuffix: true,
});
return [
{
title: document.title,
subtitle: `Updated ${time}`,
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.searchTitles(term);
return sortBy(
results.map((document: Document) => {
return {
title: document.title,
subtitle: <DocumentBreadcrumb document={document} onlyText />,
url: document.url,
};
}),
(document) =>
deburr(document.title)
.toLowerCase()
.startsWith(deburr(term).toLowerCase())
? -1
: 1
);
};
onCreateLink = async (title: string) => { onCreateLink = async (title: string) => {
const document = this.document; const document = this.document;
invariant(document, "document must be loaded to create link"); invariant(document, "document must be loaded to create link");
@@ -277,7 +223,6 @@ class DataLoader extends React.Component<Props> {
!abilities.update || !abilities.update ||
document.isArchived || document.isArchived ||
!!revisionId, !!revisionId,
onSearchLink: this.onSearchLink,
onCreateLink: this.onCreateLink, onCreateLink: this.onCreateLink,
sharedTree: this.sharedTree, sharedTree: this.sharedTree,
})} })}

View File

@@ -9,7 +9,6 @@ import { RefHandle } from "~/components/ContentEditable";
import DocumentMetaWithViews from "~/components/DocumentMetaWithViews"; import DocumentMetaWithViews from "~/components/DocumentMetaWithViews";
import Editor, { Props as EditorProps } from "~/components/Editor"; import Editor, { Props as EditorProps } from "~/components/Editor";
import Flex from "~/components/Flex"; import Flex from "~/components/Flex";
import HoverPreview from "~/components/HoverPreview";
import { import {
documentHistoryUrl, documentHistoryUrl,
documentUrl, documentUrl,
@@ -38,10 +37,6 @@ type Props = Omit<EditorProps, "extensions"> & {
* and support for hover previews of internal links. * and support for hover previews of internal links.
*/ */
function DocumentEditor(props: Props, ref: React.RefObject<any>) { function DocumentEditor(props: Props, ref: React.RefObject<any>) {
const [
activeLinkEvent,
setActiveLinkEvent,
] = React.useState<MouseEvent | null>(null);
const titleRef = React.useRef<RefHandle>(null); const titleRef = React.useRef<RefHandle>(null);
const { t } = useTranslation(); const { t } = useTranslation();
const match = useRouteMatch(); const match = useRouteMatch();
@@ -58,15 +53,6 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
} }
}, [ref]); }, [ref]);
const handleLinkActive = React.useCallback((event: MouseEvent) => {
setActiveLinkEvent(event);
return false;
}, []);
const handleLinkInactive = React.useCallback(() => {
setActiveLinkEvent(null);
}, []);
const handleGoToNextInput = React.useCallback( const handleGoToNextInput = React.useCallback(
(insertParagraph: boolean) => { (insertParagraph: boolean) => {
if (insertParagraph && ref.current) { if (insertParagraph && ref.current) {
@@ -123,7 +109,6 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
ref={ref} ref={ref}
autoFocus={!!title && !props.defaultValue} autoFocus={!!title && !props.defaultValue}
placeholder={t("Type '/' to insert, or start writing…")} placeholder={t("Type '/' to insert, or start writing…")}
onHoverLink={handleLinkActive}
scrollTo={window.location.hash} scrollTo={window.location.hash}
readOnly={readOnly} readOnly={readOnly}
shareId={shareId} shareId={shareId}
@@ -132,13 +117,6 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
{...rest} {...rest}
/> />
{!readOnly && <ClickablePadding onClick={focusAtEnd} grow />} {!readOnly && <ClickablePadding onClick={focusAtEnd} grow />}
{activeLinkEvent && !shareId && (
<HoverPreview
node={activeLinkEvent.target as HTMLAnchorElement}
event={activeLinkEvent}
onClose={handleLinkInactive}
/>
)}
{children} {children}
</Flex> </Flex>
); );