diff --git a/app/components/Breadcrumb.js b/app/components/Breadcrumb.js index 4e7c521c..350e20ad 100644 --- a/app/components/Breadcrumb.js +++ b/app/components/Breadcrumb.js @@ -1,193 +1,87 @@ // @flow -import { observer } from "mobx-react"; -import { - ArchiveIcon, - EditIcon, - GoToIcon, - ShapesIcon, - TrashIcon, -} from "outline-icons"; +import { GoToIcon } 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 Document from "models/Document"; -import CollectionIcon from "components/CollectionIcon"; import Flex from "components/Flex"; -import useStores from "hooks/useStores"; import BreadcrumbMenu from "menus/BreadcrumbMenu"; -import { collectionUrl } from "utils/routeHelpers"; -type Props = {| - document: Document, - children?: React.Node, - onlyText: boolean, +type MenuItem = {| + icon?: React.Node, + title: React.Node, + to?: string, |}; -function Icon({ document }) { - const { t } = useTranslation(); +type Props = {| + items: MenuItem[], + max?: number, + children?: React.Node, + highlightFirstItem?: boolean, +|}; - if (document.isDeleted) { - return ( - <> - - -   - {t("Trash")} - - - - ); - } - if (document.isArchived) { - return ( - <> - - -   - {t("Archive")} - - - - ); - } - if (document.isDraft) { - return ( - <> - - -   - {t("Drafts")} - - - - ); - } - if (document.isTemplate) { - return ( - <> - - -   - {t("Templates")} - - - - ); - } - return null; -} +function Breadcrumb({ items, highlightFirstItem, children, max = 2 }: Props) { + const totalItems = items.length; + let topLevelItems: MenuItem[] = [...items]; + let overflowItems; -const Breadcrumb = ({ document, children, onlyText }: Props) => { - const { collections } = useStores(); - const { t } = useTranslation(); - - if (!collections.isLoaded) { - return; + // chop middle breadcrumbs and present a "..." menu instead + if (totalItems > max) { + const halfMax = Math.floor(max / 2); + overflowItems = topLevelItems.splice(halfMax, totalItems - max); + topLevelItems.splice(halfMax, 0, { + title: , + }); } - let collection = collections.get(document.collectionId); - if (!collection) { - collection = { - id: document.collectionId, - name: t("Deleted Collection"), - color: "currentColor", - }; - } - - const path = collection.pathToDocument - ? collection.pathToDocument(document.id).slice(0, -1) - : []; - - if (onlyText === true) { - return ( - <> - {collection.name} - {path.map((n) => ( - - - {n.title} - - ))} - - ); - } - - const isNestedDocument = path.length > 1; - const lastPath = path.length ? path[path.length - 1] : undefined; - const menuPath = isNestedDocument ? path.slice(0, -1) : []; - return ( - - - -   - {collection.name} - - {isNestedDocument && ( - <> - - - )} - {lastPath && ( - <> - {" "} - - {lastPath.title} - - - )} + {topLevelItems.map((item, index) => ( + + {item.icon} + {item.to ? ( + + {item.title} + + ) : ( + item.title + )} + {index !== topLevelItems.length - 1 || !!children ? : null} + + ))} {children} ); -}; +} -export const Slash = styled(GoToIcon)` +const Slash = styled(GoToIcon)` flex-shrink: 0; fill: ${(props) => props.theme.divider}; `; -const SmallSlash = styled(GoToIcon)` - width: 12px; - height: 12px; - vertical-align: middle; - flex-shrink: 0; - - fill: ${(props) => props.theme.slate}; - opacity: 0.5; -`; - -const Crumb = styled(Link)` +const Item = styled(Link)` + display: flex; + flex-shrink: 1; + min-width: 0; color: ${(props) => props.theme.text}; font-size: 15px; height: 24px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; + font-weight: ${(props) => (props.$highlight ? "500" : "inherit")}; + margin-left: ${(props) => (props.$withIcon ? "4px" : "0")}; + + svg { + flex-shrink: 0; + } &:hover { text-decoration: underline; } `; -const CollectionName = styled(Link)` - display: flex; - flex-shrink: 1; - color: ${(props) => props.theme.text}; - font-size: 15px; - font-weight: 500; - white-space: nowrap; - overflow: hidden; - min-width: 0; - - svg { - flex-shrink: 0; - } -`; - -const CategoryName = styled(CollectionName)` - flex-shrink: 0; -`; - -export default observer(Breadcrumb); +export default Breadcrumb; diff --git a/app/components/DocumentBreadcrumb.js b/app/components/DocumentBreadcrumb.js new file mode 100644 index 00000000..5f3fdebb --- /dev/null +++ b/app/components/DocumentBreadcrumb.js @@ -0,0 +1,137 @@ +// @flow +import { observer } from "mobx-react"; +import { + ArchiveIcon, + EditIcon, + GoToIcon, + ShapesIcon, + TrashIcon, +} from "outline-icons"; +import * as React from "react"; +import { useTranslation } from "react-i18next"; +import styled from "styled-components"; +import Document from "models/Document"; +import Breadcrumb from "components/Breadcrumb"; +import CollectionIcon from "components/CollectionIcon"; +import useStores from "hooks/useStores"; +import { collectionUrl } from "utils/routeHelpers"; + +type Props = {| + document: Document, + children?: React.Node, + onlyText: boolean, +|}; + +function useCategory(document) { + const { t } = useTranslation(); + + if (document.isDeleted) { + return { + icon: , + title: t("Trash"), + to: "/trash", + }; + } + if (document.isArchived) { + return { + icon: , + title: t("Archive"), + to: "/archive", + }; + } + if (document.isDraft) { + return { + icon: , + title: t("Drafts"), + to: "/drafts", + }; + } + if (document.isTemplate) { + return { + icon: , + title: t("Templates"), + to: "/templates", + }; + } + return null; +} + +const DocumentBreadcrumb = ({ document, children, onlyText }: Props) => { + const { collections } = useStores(); + const { t } = useTranslation(); + const category = useCategory(document); + + let collection = collections.get(document.collectionId); + if (!collection) { + collection = { + id: document.collectionId, + name: t("Deleted Collection"), + color: "currentColor", + }; + } + + const path = React.useMemo( + () => + collection && collection.pathToDocument + ? collection.pathToDocument(document.id).slice(0, -1) + : [], + [collection, document.id] + ); + + const items = React.useMemo(() => { + let output = []; + + if (category) { + output.push(category); + } + + if (collection) { + output.push({ + icon: , + title: collection.name, + to: collectionUrl(collection.id), + }); + } + + path.forEach((p) => { + output.push({ + title: p.title, + to: p.url, + }); + }); + + return output; + }, [path, category, collection]); + + if (!collections.isLoaded) { + return; + } + + if (onlyText === true) { + return ( + <> + {collection.name} + {path.map((n) => ( + + + {n.title} + + ))} + + ); + } + + return ; +}; + +const SmallSlash = styled(GoToIcon)` + width: 12px; + height: 12px; + vertical-align: middle; + flex-shrink: 0; + + fill: ${(props) => props.theme.slate}; + opacity: 0.5; +`; + +export default observer(DocumentBreadcrumb); diff --git a/app/components/DocumentMeta.js b/app/components/DocumentMeta.js index b026b643..1ac34582 100644 --- a/app/components/DocumentMeta.js +++ b/app/components/DocumentMeta.js @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import styled from "styled-components"; import Document from "models/Document"; -import Breadcrumb from "components/Breadcrumb"; +import DocumentBreadcrumb from "components/DocumentBreadcrumb"; import Flex from "components/Flex"; import Time from "components/Time"; import useStores from "hooks/useStores"; @@ -142,7 +142,7 @@ function DocumentMeta({  {t("in")}  - + )} diff --git a/app/components/Editor.js b/app/components/Editor.js index 7eb7aef8..07542ba3 100644 --- a/app/components/Editor.js +++ b/app/components/Editor.js @@ -27,6 +27,7 @@ export type Props = {| grow?: boolean, disableEmbeds?: boolean, ui?: UiStore, + shareId?: ?string, autoFocus?: boolean, template?: boolean, placeholder?: string, @@ -55,7 +56,7 @@ type PropsWithRef = Props & { }; function Editor(props: PropsWithRef) { - const { id, ui, history } = props; + const { id, ui, shareId, history } = props; const { t } = useTranslation(); const isPrinting = useMediaQuery("print"); @@ -89,12 +90,16 @@ function Editor(props: PropsWithRef) { } } + if (shareId) { + navigateTo = `/share/${shareId}${navigateTo}`; + } + history.push(navigateTo); } else if (href) { window.open(href, "_blank"); } }, - [history] + [history, shareId] ); const onShowToast = React.useCallback( diff --git a/app/components/SocketProvider.js b/app/components/SocketProvider.js index 0e1c654b..c8c1808f 100644 --- a/app/components/SocketProvider.js +++ b/app/components/SocketProvider.js @@ -144,9 +144,10 @@ class SocketProvider extends React.Component { // otherwise, grab the latest version of the document try { - document = await documents.fetch(documentId, { + const response = await documents.fetch(documentId, { force: true, }); + document = response.document; } catch (err) { if (err.statusCode === 404 || err.statusCode === 403) { documents.remove(documentId); diff --git a/app/hooks/useImportDocument.js b/app/hooks/useImportDocument.js index 6f1fd91e..de68824b 100644 --- a/app/hooks/useImportDocument.js +++ b/app/hooks/useImportDocument.js @@ -36,7 +36,7 @@ export default function useImportDocument( const redirect = files.length === 1; if (documentId && !collectionId) { - const document = await documents.fetch(documentId); + const { document } = await documents.fetch(documentId); invariant(document, "Document not available"); cId = document.collectionId; } diff --git a/app/menus/BreadcrumbMenu.js b/app/menus/BreadcrumbMenu.js index 097acdc0..3ccb1b15 100644 --- a/app/menus/BreadcrumbMenu.js +++ b/app/menus/BreadcrumbMenu.js @@ -6,11 +6,17 @@ import ContextMenu from "components/ContextMenu"; import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton"; import Template from "components/ContextMenu/Template"; +type MenuItem = {| + icon?: React.Node, + title: React.Node, + to?: string, +|}; + type Props = { - path: Array, + items: MenuItem[], }; -export default function BreadcrumbMenu({ path }: Props) { +export default function BreadcrumbMenu({ items }: Props) { const { t } = useTranslation(); const menu = useMenuState({ modal: true, @@ -21,13 +27,7 @@ export default function BreadcrumbMenu({ path }: Props) { <> -