// @flow import { Search } from "js-search"; import { last } from "lodash"; import { observer } from "mobx-react"; import { TableOfContentsIcon, EditIcon, PlusIcon, MoreIcon, SearchIcon, } from "outline-icons"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import AutoSizer from "react-virtualized-auto-sizer"; import { FixedSizeList as List } from "react-window"; import { Dialog, DialogBackdrop, DialogDisclosure, useDialogState, } from "reakit"; import styled, { useTheme } from "styled-components"; import breakpoint from "styled-components-breakpoint"; import { type DocumentPath } from "stores/CollectionsStore"; import Document from "models/Document"; import { Action, Separator } from "components/Actions"; import Badge from "components/Badge"; import Button from "components/Button"; import Collaborators from "components/Collaborators"; import DocumentBreadcrumb from "components/DocumentBreadcrumb"; import Flex from "components/Flex"; import Header from "components/Header"; import PathToDocument from "components/PathToDocument"; import Tooltip from "components/Tooltip"; import PublicBreadcrumb from "./PublicBreadcrumb"; import ShareButton from "./ShareButton"; import useMobile from "hooks/useMobile"; import useStores from "hooks/useStores"; import DocumentMenu from "menus/DocumentMenu"; import NewChildDocumentMenu from "menus/NewChildDocumentMenu"; import TableOfContentsMenu from "menus/TableOfContentsMenu"; import TemplatesMenu from "menus/TemplatesMenu"; import { type NavigationNode } from "types"; import { metaDisplay } from "utils/keyboard"; import { newDocumentUrl, editDocumentUrl } from "utils/routeHelpers"; type Props = {| document: Document, sharedTree: ?NavigationNode, shareId: ?string, isDraft: boolean, isEditing: boolean, isRevision: boolean, isSaving: boolean, isPublishing: boolean, publishingIsDisabled: boolean, savingIsDisabled: boolean, onDiscard: () => void, onSave: ({ done?: boolean, publish?: boolean, autosave?: boolean, }) => void, headings: { title: string, level: number, id: string }[], |}; function DocumentHeader({ document, shareId, isEditing, isDraft, isPublishing, isRevision, isSaving, savingIsDisabled, publishingIsDisabled, sharedTree, onSave, headings, }: Props) { const { t } = useTranslation(); const { auth, ui, policies, collections, documents } = useStores(); const isMobile = useMobile(); const dialog = useDialogState({ modal: false }); const theme = useTheme(); const handleSave = React.useCallback(() => { onSave({ done: true }); }, [onSave]); const handlePublish = React.useCallback(() => { onSave({ done: true, publish: true }); }, [onSave]); const isNew = document.isNewDocument; const isTemplate = document.isTemplate; const can = policies.abilities(document.id); const canShareDocument = auth.team && auth.team.sharing && can.share; const canToggleEmbeds = auth.team && auth.team.documentEmbeds; const canEdit = can.update && !isEditing; const hasCollection = collections.get(document.computedCollectionId); const [searchTerm, setSearchTerm] = React.useState(); const handleFilter = (ev) => { setSearchTerm(ev.target.value); }; const searchIndex = React.useMemo(() => { const paths = collections.pathsToDocuments; const index = new Search("id"); index.addIndex("title"); // Build index const indexeableDocuments = []; paths.forEach((path) => { const doc = documents.get(path.id); if (!doc || !doc.isTemplate) { indexeableDocuments.push(path); } }); index.addDocuments(indexeableDocuments); return index; }, [documents, collections.pathsToDocuments]); const results: DocumentPath[] = React.useMemo(() => { const onlyShowCollections = document.isTemplate; let results = []; if (collections.isLoaded) { if (searchTerm) { results = searchIndex.search(searchTerm); } else { results = searchIndex._documents; } } if (onlyShowCollections) { results = results.filter((result) => result.type === "collection"); } else { // Exclude root from search results if document is already at the root if (!document.parentDocumentId) { results = results.filter( (result) => result.id !== document.collectionId ); } // Exclude document if on the path to result, or the same result results = results.filter( (result) => !result.path.map((doc) => doc.id).includes(document.id) && last(result.path.map((doc) => doc.id)) !== document.parentDocumentId ); } return results; }, [document, collections, searchTerm, searchIndex]); const handleSuccess = async (result: DocumentPath) => { if (!document) return; if (result.type === "collection") { onSave({ done: true, collectionId: result.collectionId }); } else { onSave({ done: true, collectionId: result.collectionId, parentDocumentId: result.id, }); } }; const row = ({ index, data, style }) => { const result = data[index]; return ( ); }; const data = results; const toc = ( ); if (shareId) { return (
{toc} } actions={canEdit ? editAction :
} /> ); } return ( <>
{!isEditing && toc} } title={ <> {document.title}{" "} {document.isArchived && {t("Archived")}} } actions={ <> {isMobile && ( )} {!isPublishing && isSaving && {t("Saving")}…} {isEditing && !isTemplate && isNew && ( )} {!isEditing && canShareDocument && (!isMobile || !isTemplate) && ( )} {can.update && isDraft && !isRevision && !hasCollection && ( Choose collection {({ width, height }) => { return ( data[index].id} > {row} ); }} )} {isEditing && ( <> )} {canEdit && editAction} {canEdit && can.createChildDocument && !isMobile && ( ( )} /> )} {canEdit && isTemplate && !isDraft && !isRevision && ( )} {can.update && isDraft && !isRevision && hasCollection && ( )} {!isEditing && ( <> (