mirror of
https://github.com/outline/outline.git
synced 2025-12-30 15:30:12 -06:00
Convert insights from sidebar to modal dialog (#9892)
* Convert insights from sidebar to modal dialog - Remove insights routing and sidebar layout - Update DocumentMeta to use modal action instead of navigation - Convert Insights component to modal-ready format - Clean up route helpers and authenticated layout - Remove insights route from authenticated routes Co-authored-by: Tom Moor <tom@getoutline.com> * Applied automatic fixes * refactor * singular * refactor --------- Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com> Co-authored-by: Tom Moor <tom@getoutline.com>
This commit is contained in:
@@ -26,7 +26,6 @@ import {
|
||||
PublishIcon,
|
||||
CommentIcon,
|
||||
CopyIcon,
|
||||
EyeIcon,
|
||||
PadlockIcon,
|
||||
GlobeIcon,
|
||||
LogoutIcon,
|
||||
@@ -70,7 +69,6 @@ import env from "~/env";
|
||||
import { setPersistedState } from "~/hooks/usePersistedState";
|
||||
import history from "~/utils/history";
|
||||
import {
|
||||
documentInsightsPath,
|
||||
documentHistoryPath,
|
||||
homePath,
|
||||
newDocumentPath,
|
||||
@@ -84,6 +82,7 @@ import {
|
||||
import capitalize from "lodash/capitalize";
|
||||
import CollectionIcon from "~/components/Icons/CollectionIcon";
|
||||
import { ActionV2, ActionV2Group, ActionV2Separator } from "~/types";
|
||||
import Insights from "~/scenes/Document/components/Insights";
|
||||
|
||||
export const openDocument = createAction({
|
||||
name: ({ t }) => t("Open document"),
|
||||
@@ -1329,7 +1328,7 @@ export const openDocumentHistory = createInternalLinkActionV2({
|
||||
},
|
||||
});
|
||||
|
||||
export const openDocumentInsights = createInternalLinkActionV2({
|
||||
export const openDocumentInsights = createActionV2({
|
||||
name: ({ t }) => t("Insights"),
|
||||
analyticsName: "Open document insights",
|
||||
section: ActiveDocumentSection,
|
||||
@@ -1347,51 +1346,17 @@ export const openDocumentInsights = createInternalLinkActionV2({
|
||||
!document?.isDeleted
|
||||
);
|
||||
},
|
||||
to: ({ activeDocumentId, stores, sidebarContext }) => {
|
||||
perform: ({ activeDocumentId, stores, t }) => {
|
||||
const document = activeDocumentId
|
||||
? stores.documents.get(activeDocumentId)
|
||||
: undefined;
|
||||
if (!document) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const [pathname, search] = documentInsightsPath(document).split("?");
|
||||
|
||||
return {
|
||||
pathname,
|
||||
search,
|
||||
state: { sidebarContext },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const toggleViewerInsights = createActionV2({
|
||||
name: ({ t, stores, activeDocumentId }) => {
|
||||
const document = activeDocumentId
|
||||
? stores.documents.get(activeDocumentId)
|
||||
: undefined;
|
||||
return document?.insightsEnabled
|
||||
? t("Disable viewer insights")
|
||||
: t("Enable viewer insights");
|
||||
},
|
||||
analyticsName: "Toggle viewer insights",
|
||||
section: ActiveDocumentSection,
|
||||
icon: <EyeIcon />,
|
||||
visible: ({ activeDocumentId, stores }) => {
|
||||
const can = stores.policies.abilities(activeDocumentId ?? "");
|
||||
return can.updateInsights;
|
||||
},
|
||||
perform: async ({ activeDocumentId, stores }) => {
|
||||
if (!activeDocumentId) {
|
||||
return;
|
||||
}
|
||||
const document = stores.documents.get(activeDocumentId);
|
||||
if (!document) {
|
||||
return;
|
||||
}
|
||||
|
||||
await document.save({
|
||||
insightsEnabled: !document.insightsEnabled,
|
||||
stores.dialogs.openModal({
|
||||
title: t("Insights"),
|
||||
content: <Insights document={document} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
settingsPath,
|
||||
matchDocumentHistory,
|
||||
matchDocumentSlug as slug,
|
||||
matchDocumentInsights,
|
||||
} from "~/utils/routeHelpers";
|
||||
import { DocumentContextProvider } from "./DocumentContext";
|
||||
import Fade from "./Fade";
|
||||
@@ -39,9 +38,7 @@ const DocumentComments = lazyWithRetry(
|
||||
const DocumentHistory = lazyWithRetry(
|
||||
() => import("~/scenes/Document/components/History")
|
||||
);
|
||||
const DocumentInsights = lazyWithRetry(
|
||||
() => import("~/scenes/Document/components/Insights")
|
||||
);
|
||||
|
||||
const CommandBar = lazyWithRetry(() => import("~/components/CommandBar"));
|
||||
|
||||
type Props = {
|
||||
@@ -98,12 +95,7 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
|
||||
!!matchPath(location.pathname, {
|
||||
path: matchDocumentHistory,
|
||||
}) && can.listRevisions;
|
||||
const showInsights =
|
||||
!!matchPath(location.pathname, {
|
||||
path: matchDocumentInsights,
|
||||
}) && can.listViews;
|
||||
const showComments =
|
||||
!showInsights &&
|
||||
!showHistory &&
|
||||
can.comment &&
|
||||
ui.activeDocumentId &&
|
||||
@@ -115,12 +107,11 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
|
||||
initial={false}
|
||||
key={ui.activeDocumentId ? "active" : "inactive"}
|
||||
>
|
||||
{(showHistory || showInsights || showComments) && (
|
||||
{(showHistory || showComments) && (
|
||||
<Route path={`/doc/${slug}`}>
|
||||
<SidebarRight>
|
||||
<React.Suspense fallback={null}>
|
||||
{showHistory && <DocumentHistory />}
|
||||
{showInsights && <DocumentInsights />}
|
||||
{showComments && <DocumentComments />}
|
||||
</React.Suspense>
|
||||
</SidebarRight>
|
||||
|
||||
@@ -148,6 +148,10 @@ function Collaborators(props: Props) {
|
||||
[presentIds, editingIds, observingUserId, currentUserId, handleAvatarClick]
|
||||
);
|
||||
|
||||
if (!document.insightsEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
|
||||
16
app/hooks/useFormatNumber.ts
Normal file
16
app/hooks/useFormatNumber.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { formatNumber } from "~/utils/language";
|
||||
import useUserLocale from "./useUserLocale";
|
||||
import { unicodeCLDRtoBCP47 } from "@shared/utils/date";
|
||||
|
||||
/**
|
||||
* Hook that returns a function to format numbers based on the user's locale.
|
||||
*
|
||||
* @returns A function that formats numbers
|
||||
*/
|
||||
export function useFormatNumber() {
|
||||
const language = useUserLocale();
|
||||
return (input: number) =>
|
||||
language
|
||||
? formatNumber(input, unicodeCLDRtoBCP47(language))
|
||||
: input.toString();
|
||||
}
|
||||
@@ -148,6 +148,13 @@ function DocumentMenu({
|
||||
[user, document]
|
||||
);
|
||||
|
||||
const handleInsightsToggle = React.useCallback(
|
||||
(checked: boolean) => {
|
||||
void document.save({ insightsEnabled: checked });
|
||||
},
|
||||
[document]
|
||||
);
|
||||
|
||||
const templateMenuActions = useTemplateMenuActions({
|
||||
document,
|
||||
onSelectTemplate,
|
||||
@@ -231,6 +238,18 @@ function DocumentMenu({
|
||||
<>
|
||||
<MenuSeparator />
|
||||
<DisplayOptions>
|
||||
{can.updateInsights && (
|
||||
<Style>
|
||||
<ToggleMenuItem
|
||||
width={26}
|
||||
height={14}
|
||||
label={t("Enable viewer insights")}
|
||||
labelPosition="left"
|
||||
checked={document.insightsEnabled}
|
||||
onChange={handleInsightsToggle}
|
||||
/>
|
||||
</Style>
|
||||
)}
|
||||
{showToggleEmbeds && (
|
||||
<Style>
|
||||
<ToggleMenuItem
|
||||
@@ -263,6 +282,7 @@ function DocumentMenu({
|
||||
can.update,
|
||||
document.embedsDisabled,
|
||||
document.fullWidth,
|
||||
document.insightsEnabled,
|
||||
isMobile,
|
||||
showDisplayOptions,
|
||||
showToggleEmbeds,
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { t } from "i18next";
|
||||
import { MoreIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { s, hover } from "@shared/styles";
|
||||
import { DropdownMenu } from "~/components/Menu/DropdownMenu";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import { toggleViewerInsights } from "~/actions/definitions/documents";
|
||||
import { useMemo } from "react";
|
||||
import { useMenuAction } from "~/hooks/useMenuAction";
|
||||
|
||||
const InsightsMenu: React.FC = () => {
|
||||
const actions = useMemo(() => [toggleViewerInsights], []);
|
||||
const rootAction = useMenuAction(actions);
|
||||
|
||||
return (
|
||||
<DropdownMenu action={rootAction} align="end" ariaLabel={t("Insights")}>
|
||||
<Button>
|
||||
<MoreIcon />
|
||||
</Button>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
const Button = styled(NudeButton)`
|
||||
color: ${s("textSecondary")};
|
||||
|
||||
&:${hover},
|
||||
&:active,
|
||||
&[data-state="open"] {
|
||||
color: ${s("text")};
|
||||
background: ${s("sidebarControlHoverBackground")};
|
||||
}
|
||||
`;
|
||||
|
||||
export default InsightsMenu;
|
||||
@@ -93,11 +93,7 @@ function AuthenticatedRoutes() {
|
||||
path={`/doc/${slug}/history/:revisionId?`}
|
||||
component={Document}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`/doc/${slug}/insights`}
|
||||
component={Document}
|
||||
/>
|
||||
|
||||
<Route exact path={`/doc/${slug}/edit`} component={Document} />
|
||||
<Route path={`/doc/${slug}`} component={Document} />
|
||||
<Route
|
||||
|
||||
@@ -8,13 +8,15 @@ import styled from "styled-components";
|
||||
import { TeamPreference } from "@shared/types";
|
||||
import Document from "~/models/Document";
|
||||
import Revision from "~/models/Revision";
|
||||
import { openDocumentInsights } from "~/actions/definitions/documents";
|
||||
import DocumentMeta from "~/components/DocumentMeta";
|
||||
import Fade from "~/components/Fade";
|
||||
import useActionContext from "~/hooks/useActionContext";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { documentPath, documentInsightsPath } from "~/utils/routeHelpers";
|
||||
import { documentPath } from "~/utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
/* The document to display meta data for */
|
||||
@@ -35,10 +37,12 @@ function TitleDocumentMeta({ to, document, revision, ...rest }: Props) {
|
||||
const onlyYou = totalViewers === 1 && documentViews[0].userId;
|
||||
const viewsLoadedOnMount = useRef(totalViewers > 0);
|
||||
const can = usePolicy(document);
|
||||
const actionContext = useActionContext({
|
||||
activeDocumentId: document.id,
|
||||
});
|
||||
|
||||
const Wrapper = viewsLoadedOnMount.current ? Fragment : Fade;
|
||||
|
||||
const insightsPath = documentInsightsPath(document);
|
||||
const commentsCount = comments.unresolvedCommentsInDocumentCount(document.id);
|
||||
const commentingEnabled = !!team.getPreference(TeamPreference.Commenting);
|
||||
|
||||
@@ -67,14 +71,8 @@ function TitleDocumentMeta({ to, document, revision, ...rest }: Props) {
|
||||
!document.isTemplate ? (
|
||||
<Wrapper>
|
||||
•
|
||||
<Link
|
||||
to={{
|
||||
pathname:
|
||||
match.url === insightsPath
|
||||
? documentPath(document)
|
||||
: insightsPath,
|
||||
state: { sidebarContext },
|
||||
}}
|
||||
<InsightsButton
|
||||
onClick={() => openDocumentInsights.perform(actionContext)}
|
||||
>
|
||||
{t("Viewed by")}{" "}
|
||||
{onlyYou
|
||||
@@ -82,7 +80,7 @@ function TitleDocumentMeta({ to, document, revision, ...rest }: Props) {
|
||||
: `${totalViewers} ${
|
||||
totalViewers === 1 ? t("person") : t("people")
|
||||
}`}
|
||||
</Link>
|
||||
</InsightsButton>
|
||||
</Wrapper>
|
||||
) : null}
|
||||
</Meta>
|
||||
@@ -94,6 +92,20 @@ const CommentLink = styled(Link)`
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const InsightsButton = styled.button`
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-decoration: none;
|
||||
cursor: var(--pointer);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Meta = styled(DocumentMeta)<{ rtl?: boolean }>`
|
||||
justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")};
|
||||
margin: -12px 0 2em 0;
|
||||
|
||||
@@ -1,55 +1,33 @@
|
||||
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";
|
||||
import type Document from "~/models/Document";
|
||||
import { useFormatNumber } from "~/hooks/useFormatNumber";
|
||||
|
||||
function Insights() {
|
||||
const { views, documents } = useStores();
|
||||
type Props = {
|
||||
document: Document;
|
||||
};
|
||||
|
||||
function Insights({ document }: Props) {
|
||||
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 text = document.toPlainText();
|
||||
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);
|
||||
const formatNumber = useFormatNumber();
|
||||
|
||||
return (
|
||||
<Sidebar title={t("Insights")} onClose={onCloseInsights}>
|
||||
<div>
|
||||
{document ? (
|
||||
<Flex
|
||||
column
|
||||
@@ -58,69 +36,86 @@ function Insights() {
|
||||
justify="space-between"
|
||||
>
|
||||
<div>
|
||||
<Content column>
|
||||
{document.sourceMetadata && (
|
||||
<>
|
||||
<Heading>{t("Source")}</Heading>
|
||||
{
|
||||
<Text as="p" type="secondary" size="small">
|
||||
<Flex column>
|
||||
<Text as="h2" size="large">
|
||||
{t("Source")}
|
||||
</Text>
|
||||
<Text as="p" type="secondary" size="small">
|
||||
<List>
|
||||
<li>
|
||||
{t("Created")}{" "}
|
||||
<Time dateTime={document.createdAt} addSuffix />
|
||||
</li>
|
||||
<li>
|
||||
{t(`Last updated`)}{" "}
|
||||
<Time dateTime={document.updatedAt} addSuffix />
|
||||
</li>
|
||||
{document.sourceMetadata && (
|
||||
<li>
|
||||
{t("Imported from {{ source }}", {
|
||||
source:
|
||||
document.sourceName ??
|
||||
`“${document.sourceMetadata.fileName}”`,
|
||||
})}
|
||||
</Text>
|
||||
}
|
||||
</>
|
||||
)}
|
||||
<Heading>{t("Stats")}</Heading>
|
||||
</li>
|
||||
)}
|
||||
</List>
|
||||
</Text>
|
||||
|
||||
<Text as="h2" size="large">
|
||||
{t("Stats")}
|
||||
</Text>
|
||||
<Text as="p" type="secondary" size="small">
|
||||
<List>
|
||||
{stats.total.words > 0 && (
|
||||
<li>
|
||||
{t(`{{ count }} minute read`, {
|
||||
count: stats.total.readingTime,
|
||||
{t(`{{ number }} minute read`, {
|
||||
number: formatNumber(stats.total.readingTime),
|
||||
})}
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
{t(`{{ count }} words`, { count: stats.total.words })}
|
||||
</li>
|
||||
<li>
|
||||
{t(`{{ count }} characters`, {
|
||||
count: stats.total.characters,
|
||||
{t(`{{ number }} words`, {
|
||||
count: stats.total.words,
|
||||
number: formatNumber(stats.total.words),
|
||||
})}
|
||||
</li>
|
||||
<li>
|
||||
{t(`{{ number }} emoji`, { number: stats.total.emoji })}
|
||||
{t(`{{ number }} characters`, {
|
||||
count: stats.total.characters,
|
||||
number: formatNumber(stats.total.characters),
|
||||
})}
|
||||
</li>
|
||||
<li>
|
||||
{t(`{{ number }} emoji`, {
|
||||
number: formatNumber(stats.total.emoji),
|
||||
})}
|
||||
</li>
|
||||
{stats.selected.characters === 0 ? (
|
||||
<li>{t("No text selected")}</li>
|
||||
) : (
|
||||
<>
|
||||
<li>
|
||||
{t(`{{ count }} words selected`, {
|
||||
{t(`{{ number }} words selected`, {
|
||||
count: stats.selected.words,
|
||||
number: formatNumber(stats.selected.words),
|
||||
})}
|
||||
</li>
|
||||
<li>
|
||||
{t(`{{ count }} characters selected`, {
|
||||
{t(`{{ number }} characters selected`, {
|
||||
count: stats.selected.characters,
|
||||
number: formatNumber(stats.selected.characters),
|
||||
})}
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</List>
|
||||
</Text>
|
||||
</Content>
|
||||
</Flex>
|
||||
|
||||
<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 />.
|
||||
<Flex column>
|
||||
<Text as="h2" size="large">
|
||||
{t("Contributors")}
|
||||
</Text>
|
||||
<ListSpacing>
|
||||
{document.sourceMetadata?.createdByName && (
|
||||
@@ -166,49 +161,11 @@ function Insights() {
|
||||
)}
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
) : null}
|
||||
</Sidebar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -218,7 +175,7 @@ const ListSpacing = styled("div")`
|
||||
`;
|
||||
|
||||
const List = styled("ul")`
|
||||
margin: 0;
|
||||
margin: 0 0 1em;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
@@ -231,13 +188,4 @@ const List = styled("ul")`
|
||||
}
|
||||
`;
|
||||
|
||||
const Content = styled(Flex)`
|
||||
padding: 0 16px;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
const Heading = styled("h3")`
|
||||
font-size: 15px;
|
||||
`;
|
||||
|
||||
export default observer(Insights);
|
||||
|
||||
@@ -13,9 +13,8 @@ import {
|
||||
} from "~/components/SortableTable";
|
||||
import { type Column as TableColumn } from "~/components/Table";
|
||||
import Time from "~/components/Time";
|
||||
import useUserLocale from "~/hooks/useUserLocale";
|
||||
import ShareMenu from "~/menus/ShareMenu";
|
||||
import { formatNumber } from "~/utils/language";
|
||||
import { useFormatNumber } from "~/hooks/useFormatNumber";
|
||||
|
||||
const ROW_HEIGHT = 50;
|
||||
|
||||
@@ -25,7 +24,7 @@ type Props = Omit<TableProps<Share>, "columns" | "rowHeight"> & {
|
||||
|
||||
export function SharesTable({ data, canManage, ...rest }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const language = useUserLocale();
|
||||
const formatNumber = useFormatNumber();
|
||||
const hasDomain = data.some((share) => share.domain);
|
||||
|
||||
const columns = useMemo<TableColumn<Share>[]>(
|
||||
@@ -101,13 +100,7 @@ export function SharesTable({ data, canManage, ...rest }: Props) {
|
||||
id: "views",
|
||||
header: t("Views"),
|
||||
accessor: (share) => share.views,
|
||||
component: (share) => (
|
||||
<>
|
||||
{language
|
||||
? formatNumber(share.views, unicodeCLDRtoBCP47(language))
|
||||
: share.views}
|
||||
</>
|
||||
),
|
||||
component: (share) => formatNumber(share.views),
|
||||
width: "150px",
|
||||
},
|
||||
canManage
|
||||
@@ -123,7 +116,7 @@ export function SharesTable({ data, canManage, ...rest }: Props) {
|
||||
}
|
||||
: undefined,
|
||||
]),
|
||||
[t, language, hasDomain, canManage]
|
||||
[t, hasDomain, canManage]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { i18n } from "i18next";
|
||||
import { locales, unicodeCLDRtoBCP47 } from "@shared/utils/date";
|
||||
import Desktop from "./Desktop";
|
||||
import User from "~/models/User";
|
||||
import useUserLocale from "~/hooks/useUserLocale";
|
||||
|
||||
/**
|
||||
* Formats a number using the user's locale where possible.
|
||||
* Formats a number using the user's locale where possible. Use `useFormatNumber` hook
|
||||
* instead of this function in React components, to automatically use the user's locale.
|
||||
*
|
||||
* @param number The number to format
|
||||
* @param locale The locale to use for formatting (BCP47 format)
|
||||
|
||||
@@ -63,9 +63,7 @@ export function documentEditPath(doc: Document): string {
|
||||
return `${documentPath(doc)}/edit`;
|
||||
}
|
||||
|
||||
export function documentInsightsPath(doc: Document): string {
|
||||
return `${documentPath(doc)}/insights`;
|
||||
}
|
||||
|
||||
|
||||
export function documentHistoryPath(
|
||||
doc: Document,
|
||||
@@ -155,4 +153,4 @@ export const matchDocumentEdit = `/doc/${matchDocumentSlug}/edit`;
|
||||
|
||||
export const matchDocumentHistory = `/doc/${matchDocumentSlug}/history/:revisionId?`;
|
||||
|
||||
export const matchDocumentInsights = `/doc/${matchDocumentSlug}/insights`;
|
||||
|
||||
|
||||
@@ -105,8 +105,6 @@
|
||||
"Comments": "Comments",
|
||||
"History": "History",
|
||||
"Insights": "Insights",
|
||||
"Disable viewer insights": "Disable viewer insights",
|
||||
"Enable viewer insights": "Enable viewer insights",
|
||||
"Leave document": "Leave document",
|
||||
"You have left the shared document": "You have left the shared document",
|
||||
"Could not leave document": "Could not leave document",
|
||||
@@ -567,6 +565,7 @@
|
||||
"Rename": "Rename",
|
||||
"Collection menu": "Collection menu",
|
||||
"Comment options": "Comment options",
|
||||
"Enable viewer insights": "Enable viewer insights",
|
||||
"Enable embeds": "Enable embeds",
|
||||
"Document options": "Document options",
|
||||
"File": "File",
|
||||
@@ -688,29 +687,24 @@
|
||||
"Restore version": "Restore version",
|
||||
"No history yet": "No history yet",
|
||||
"Source": "Source",
|
||||
"Created": "Created",
|
||||
"Imported from {{ source }}": "Imported from {{ source }}",
|
||||
"Stats": "Stats",
|
||||
"{{ count }} minute read": "{{ count }} minute read",
|
||||
"{{ count }} minute read_plural": "{{ count }} minute read",
|
||||
"{{ count }} words": "{{ count }} word",
|
||||
"{{ count }} words_plural": "{{ count }} words",
|
||||
"{{ count }} characters": "{{ count }} character",
|
||||
"{{ count }} characters_plural": "{{ count }} characters",
|
||||
"{{ number }} minute read": "{{ number }} minute read",
|
||||
"{{ number }} words": "{{ number }} word",
|
||||
"{{ number }} words_plural": "{{ number }} words",
|
||||
"{{ number }} characters": "{{ number }} character",
|
||||
"{{ number }} characters_plural": "{{ number }} characters",
|
||||
"{{ number }} emoji": "{{ number }} emoji",
|
||||
"No text selected": "No text selected",
|
||||
"{{ count }} words selected": "{{ count }} word selected",
|
||||
"{{ count }} words selected_plural": "{{ count }} words selected",
|
||||
"{{ count }} characters selected": "{{ count }} character selected",
|
||||
"{{ count }} characters selected_plural": "{{ count }} characters selected",
|
||||
"{{ number }} words selected": "{{ number }} word selected",
|
||||
"{{ number }} words selected_plural": "{{ number }} words selected",
|
||||
"{{ number }} characters selected": "{{ number }} character selected",
|
||||
"{{ number }} characters selected_plural": "{{ number }} characters selected",
|
||||
"Contributors": "Contributors",
|
||||
"Created": "Created",
|
||||
"Creator": "Creator",
|
||||
"Last edited": "Last edited",
|
||||
"Previously edited": "Previously edited",
|
||||
"No one else has viewed yet": "No one else has viewed yet",
|
||||
"Viewed {{ count }} times by {{ teamMembers }} people": "Viewed {{ count }} time by {{ teamMembers }} people",
|
||||
"Viewed {{ count }} times by {{ teamMembers }} people_plural": "Viewed {{ count }} times by {{ teamMembers }} people",
|
||||
"Viewer insights are disabled.": "Viewer insights are disabled.",
|
||||
"Sorry, the last change could not be persisted – please reload the page": "Sorry, the last change could not be persisted – please reload the page",
|
||||
"{{ count }} days": "{{ count }} day",
|
||||
"{{ count }} days_plural": "{{ count }} days",
|
||||
|
||||
Reference in New Issue
Block a user