mirror of
https://github.com/outline/outline.git
synced 2025-12-21 10:39:41 -06:00
* Upgrade Prettier to v3.6.2 and eslint-plugin-prettier to v5.5.1 - Upgraded prettier from ^2.8.8 to ^3.6.2 (latest version) - Upgraded eslint-plugin-prettier from ^4.2.1 to ^5.5.1 for compatibility - Applied automatic formatting changes from new Prettier version - All existing ESLint and Prettier configurations remain compatible * Applied automatic fixes * Trigger CI --------- Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com> Co-authored-by: Tom Moor <tom@getoutline.com>
244 lines
8.3 KiB
TypeScript
244 lines
8.3 KiB
TypeScript
import { observer } from "mobx-react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useHistory, useRouteMatch } from "react-router-dom";
|
|
import styled from "styled-components";
|
|
import { s } from "@shared/styles";
|
|
import { stringToColor } from "@shared/utils/color";
|
|
import User from "~/models/User";
|
|
import { Avatar, AvatarSize } from "~/components/Avatar";
|
|
import { useDocumentContext } from "~/components/DocumentContext";
|
|
import DocumentViews from "~/components/DocumentViews";
|
|
import Flex from "~/components/Flex";
|
|
import ListItem from "~/components/List/Item";
|
|
import PaginatedList from "~/components/PaginatedList";
|
|
import Text from "~/components/Text";
|
|
import Time from "~/components/Time";
|
|
import useKeyDown from "~/hooks/useKeyDown";
|
|
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
|
|
import usePolicy from "~/hooks/usePolicy";
|
|
import useStores from "~/hooks/useStores";
|
|
import useTextSelection from "~/hooks/useTextSelection";
|
|
import { useTextStats } from "~/hooks/useTextStats";
|
|
import InsightsMenu from "~/menus/InsightsMenu";
|
|
import { documentPath } from "~/utils/routeHelpers";
|
|
import Sidebar from "./SidebarLayout";
|
|
|
|
function Insights() {
|
|
const { views, documents } = useStores();
|
|
const { t } = useTranslation();
|
|
const match = useRouteMatch<{ documentSlug: string }>();
|
|
const history = useHistory();
|
|
const sidebarContext = useLocationSidebarContext();
|
|
const selectedText = useTextSelection();
|
|
const document = documents.getByUrl(match.params.documentSlug);
|
|
const { editor } = useDocumentContext();
|
|
const text = editor?.getPlainText();
|
|
const stats = useTextStats(text ?? "", selectedText);
|
|
const can = usePolicy(document);
|
|
const documentViews = document ? views.inDocument(document.id) : [];
|
|
|
|
const onCloseInsights = () => {
|
|
if (document) {
|
|
history.push({
|
|
pathname: documentPath(document),
|
|
state: { sidebarContext },
|
|
});
|
|
}
|
|
};
|
|
|
|
useKeyDown("Escape", onCloseInsights);
|
|
|
|
return (
|
|
<Sidebar title={t("Insights")} onClose={onCloseInsights}>
|
|
{document ? (
|
|
<Flex
|
|
column
|
|
shrink={false}
|
|
style={{ minHeight: "100%" }}
|
|
justify="space-between"
|
|
>
|
|
<div>
|
|
<Content column>
|
|
{document.sourceMetadata && (
|
|
<>
|
|
<Heading>{t("Source")}</Heading>
|
|
{
|
|
<Text as="p" type="secondary" size="small">
|
|
{t("Imported from {{ source }}", {
|
|
source:
|
|
document.sourceName ??
|
|
`“${document.sourceMetadata.fileName}”`,
|
|
})}
|
|
</Text>
|
|
}
|
|
</>
|
|
)}
|
|
<Heading>{t("Stats")}</Heading>
|
|
<Text as="p" type="secondary" size="small">
|
|
<List>
|
|
{stats.total.words > 0 && (
|
|
<li>
|
|
{t(`{{ count }} minute read`, {
|
|
count: stats.total.readingTime,
|
|
})}
|
|
</li>
|
|
)}
|
|
<li>
|
|
{t(`{{ count }} words`, { count: stats.total.words })}
|
|
</li>
|
|
<li>
|
|
{t(`{{ count }} characters`, {
|
|
count: stats.total.characters,
|
|
})}
|
|
</li>
|
|
<li>
|
|
{t(`{{ number }} emoji`, { number: stats.total.emoji })}
|
|
</li>
|
|
{stats.selected.characters === 0 ? (
|
|
<li>{t("No text selected")}</li>
|
|
) : (
|
|
<>
|
|
<li>
|
|
{t(`{{ count }} words selected`, {
|
|
count: stats.selected.words,
|
|
})}
|
|
</li>
|
|
<li>
|
|
{t(`{{ count }} characters selected`, {
|
|
count: stats.selected.characters,
|
|
})}
|
|
</li>
|
|
</>
|
|
)}
|
|
</List>
|
|
</Text>
|
|
</Content>
|
|
|
|
<Content column>
|
|
<Heading>{t("Contributors")}</Heading>
|
|
<Text as="p" type="secondary" size="small">
|
|
{t(`Created`)} <Time dateTime={document.createdAt} addSuffix />.
|
|
<br />
|
|
{t(`Last updated`)}{" "}
|
|
<Time dateTime={document.updatedAt} addSuffix />.
|
|
</Text>
|
|
<ListSpacing>
|
|
{document.sourceMetadata?.createdByName && (
|
|
<ListItem
|
|
title={document.sourceMetadata?.createdByName}
|
|
image={
|
|
<Avatar
|
|
model={{
|
|
color: stringToColor(
|
|
document.sourceMetadata.createdByName
|
|
),
|
|
avatarUrl: null,
|
|
initial: document.sourceMetadata.createdByName[0],
|
|
}}
|
|
size={AvatarSize.Large}
|
|
/>
|
|
}
|
|
subtitle={t("Creator")}
|
|
border={false}
|
|
small
|
|
/>
|
|
)}
|
|
<PaginatedList<User>
|
|
aria-label={t("Contributors")}
|
|
items={document.collaborators}
|
|
renderItem={(model) => (
|
|
<ListItem
|
|
key={model.id}
|
|
title={model.name}
|
|
image={<Avatar model={model} size={32} />}
|
|
subtitle={
|
|
model.id === document.createdBy?.id
|
|
? document.sourceMetadata?.createdByName
|
|
? t("Imported")
|
|
: t("Creator")
|
|
: model.id === document.updatedBy?.id
|
|
? t("Last edited")
|
|
: t("Previously edited")
|
|
}
|
|
border={false}
|
|
small
|
|
/>
|
|
)}
|
|
/>
|
|
</ListSpacing>
|
|
</Content>
|
|
{(document.insightsEnabled || can.updateInsights) && (
|
|
<Content column>
|
|
<Heading>
|
|
<Flex justify="space-between">
|
|
{t("Viewed by")}
|
|
{can.updateInsights && <InsightsMenu />}
|
|
</Flex>
|
|
</Heading>
|
|
{document.insightsEnabled ? (
|
|
<>
|
|
<Text as="p" type="secondary" size="small">
|
|
{documentViews.length <= 1
|
|
? t("No one else has viewed yet")
|
|
: t(
|
|
`Viewed {{ count }} times by {{ teamMembers }} people`,
|
|
{
|
|
count: documentViews.reduce(
|
|
(memo, view) => memo + view.count,
|
|
0
|
|
),
|
|
teamMembers: documentViews.length,
|
|
}
|
|
)}
|
|
.
|
|
</Text>
|
|
{documentViews.length > 1 && (
|
|
<ListSpacing>
|
|
<DocumentViews document={document} />
|
|
</ListSpacing>
|
|
)}
|
|
</>
|
|
) : (
|
|
<Text as="p" type="secondary" size="small">
|
|
{t("Viewer insights are disabled.")}
|
|
</Text>
|
|
)}
|
|
</Content>
|
|
)}
|
|
</div>
|
|
</Flex>
|
|
) : null}
|
|
</Sidebar>
|
|
);
|
|
}
|
|
|
|
const ListSpacing = styled("div")`
|
|
margin-top: -0.5em;
|
|
margin-bottom: 0.5em;
|
|
`;
|
|
|
|
const List = styled("ul")`
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
|
|
li:before {
|
|
content: "·";
|
|
display: inline-block;
|
|
font-weight: 600;
|
|
color: ${s("textTertiary")};
|
|
width: 10px;
|
|
}
|
|
`;
|
|
|
|
const Content = styled(Flex)`
|
|
padding: 0 16px;
|
|
user-select: none;
|
|
`;
|
|
|
|
const Heading = styled("h3")`
|
|
font-size: 15px;
|
|
`;
|
|
|
|
export default observer(Insights);
|