// @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, 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 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 handleSave = React.useCallback(() => {
onSave({ done: true });
}, [onSave]);
const handlePublish = React.useCallback(() => {
if (!hasCollection) {
dialog.setVisible(true);
return;
}
onSave({ done: true, publish: true });
}, [dialog, hasCollection, onSave]);
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;
dialog.setVisible(false);
if (result.type === "collection") {
onSave({ done: true, publish: true, collectionId: result.collectionId });
} else {
onSave({
done: true,
publish: true,
collectionId: result.collectionId,
parentDocumentId: result.id,
});
}
};
const row = ({ index, data, style }) => {
const result = data[index];
return (
);
};
const data = results;
const toc = (
}
iconColor="currentColor"
borderOnHover
neutral
/>
);
const editAction = (
}
to={editDocumentUrl(document)}
neutral
>
{t("Edit")}
);
if (shareId) {
return (
{toc}
}
actions={canEdit ? editAction : }
/>
);
}
return (
<>