mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
7ed41eadc6
* feat: add title and logoUrl to Share model Agent-Logs-Url: https://github.com/outline/outline/sessions/9bc9d438-6892-4903-9d32-6b6868f4fd97 Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> * fix: use STRING(4096) for logoUrl column in migration Agent-Logs-Url: https://github.com/outline/outline/sessions/9bc9d438-6892-4903-9d32-6b6868f4fd97 Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> * feat: use share title and logoUrl to override team branding on shared page Agent-Logs-Url: https://github.com/outline/outline/sessions/854d6d22-e80b-4673-b3b2-0f9cf43a3246 Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> * refactor: use ShareValidation class constants for title/logoUrl max lengths Agent-Logs-Url: https://github.com/outline/outline/sessions/ea462d6a-d4d3-4882-ab8e-88060bf64877 Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> * fix: use ShareValidation constants in @Length msg template literals Agent-Logs-Url: https://github.com/outline/outline/sessions/694116c2-47e8-4001-a103-c8a62c7ac71e Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> * feat: add display settings popover with custom title and icon for shares Move share toggles (search indexing, email subscriptions, show last modified, show TOC) into a popover triggered by a settings cog. The popover also includes inputs for a custom site title and icon upload to override team branding on shared pages. Rename logoUrl to iconUrl, loosen URL validation to allow relative attachment paths, and surface the popover in the shared page header for users with edit permission. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * styling * Display branding on single shared pages * Review comments * refactor * PR feedback * Lose 'Remove icon' button --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> Co-authored-by: Tom Moor <tom@getoutline.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
209 lines
5.9 KiB
TypeScript
209 lines
5.9 KiB
TypeScript
import { observer } from "mobx-react";
|
|
import { TableOfContentsIcon, EditIcon, SettingsIcon } from "outline-icons";
|
|
import { useCallback, useEffect } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Link } from "react-router-dom";
|
|
import useMeasure from "react-use-measure";
|
|
import styled from "styled-components";
|
|
import Icon from "@shared/components/Icon";
|
|
import useShare from "@shared/hooks/useShare";
|
|
import { altDisplay } from "@shared/utils/keyboard";
|
|
import { Action } from "~/components/Actions";
|
|
import Button from "~/components/Button";
|
|
import { useDocumentContext } from "~/components/DocumentContext";
|
|
import Flex from "~/components/Flex";
|
|
import Header from "~/components/Header";
|
|
import {
|
|
AppearanceAction,
|
|
SubscribeAction,
|
|
} from "~/components/Sharing/components/Actions";
|
|
import HeaderBranding from "~/components/Sharing/components/HeaderBranding";
|
|
import ShareSettingsPopover from "~/components/Sharing/components/ShareSettingsPopover";
|
|
import Tooltip from "~/components/Tooltip";
|
|
import env from "~/env";
|
|
import useCurrentUser from "~/hooks/useCurrentUser";
|
|
import useEditingFocus from "~/hooks/useEditingFocus";
|
|
import useKeyDown from "~/hooks/useKeyDown";
|
|
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
|
|
import useMobile from "~/hooks/useMobile";
|
|
import usePolicy from "~/hooks/usePolicy";
|
|
import useStores from "~/hooks/useStores";
|
|
import TableOfContentsMenu from "~/menus/TableOfContentsMenu";
|
|
import type Document from "~/models/Document";
|
|
import { documentEditPath } from "~/utils/routeHelpers";
|
|
import PublicBreadcrumb from "./PublicBreadcrumb";
|
|
|
|
type Props = {
|
|
document: Document;
|
|
};
|
|
|
|
function SharedDocumentHeader({ document }: Props) {
|
|
const { t } = useTranslation();
|
|
const { ui, shares } = useStores();
|
|
const user = useCurrentUser({ rejectOnEmpty: false });
|
|
const isMobileMedia = useMobile();
|
|
const isEditingFocus = useEditingFocus();
|
|
|
|
// Set CSS variable for header offset (used by sticky table headers)
|
|
useEffect(() => {
|
|
window.document.documentElement.style.setProperty(
|
|
"--header-offset",
|
|
isEditingFocus ? "0px" : "64px"
|
|
);
|
|
}, [isEditingFocus]);
|
|
|
|
const { hasHeadings } = useDocumentContext();
|
|
const sidebarContext = useLocationSidebarContext();
|
|
const [measureRef, size] = useMeasure();
|
|
const { shareId, sharedTree, allowSubscriptions } = useShare();
|
|
const share = shareId ? shares.get(shareId) : undefined;
|
|
const isMobile = isMobileMedia || (size.width > 0 && size.width < 700);
|
|
|
|
const handleToggle = useCallback(() => {
|
|
// Public shares, by default, show ToC on load.
|
|
if (ui.tocVisible === undefined) {
|
|
ui.set({ tocVisible: false });
|
|
} else {
|
|
ui.set({ tocVisible: !ui.tocVisible });
|
|
}
|
|
}, [ui]);
|
|
|
|
const can = usePolicy(document);
|
|
const showContents = ui.tocVisible !== false;
|
|
|
|
useEffect(() => {
|
|
if (isMobile && showContents) {
|
|
ui.set({ tocVisible: false });
|
|
}
|
|
}, [isMobile, showContents, ui]);
|
|
|
|
useKeyDown(
|
|
(event) => event.ctrlKey && event.altKey && event.code === "KeyH",
|
|
handleToggle,
|
|
{
|
|
allowInInput: true,
|
|
}
|
|
);
|
|
|
|
if (!shareId) {
|
|
return null;
|
|
}
|
|
|
|
const toc = (
|
|
<Tooltip
|
|
content={
|
|
showContents
|
|
? t("Hide contents")
|
|
: hasHeadings
|
|
? t("Show contents")
|
|
: `${t("Show contents")} (${t("available when headings are added")})`
|
|
}
|
|
shortcut={`Ctrl+${altDisplay}+h`}
|
|
placement="bottom"
|
|
>
|
|
<Button
|
|
aria-label={t("Show contents")}
|
|
onClick={handleToggle}
|
|
icon={<TableOfContentsIcon />}
|
|
borderOnHover
|
|
neutral
|
|
/>
|
|
</Tooltip>
|
|
);
|
|
|
|
const editAction = can.update ? (
|
|
<Action>
|
|
<Tooltip
|
|
content={t("Edit {{noun}}", { noun: document.noun })}
|
|
shortcut="e"
|
|
placement="bottom"
|
|
>
|
|
<Button
|
|
as={Link}
|
|
icon={<EditIcon />}
|
|
to={{
|
|
pathname: documentEditPath(document),
|
|
state: { sidebarContext },
|
|
}}
|
|
haptic="light"
|
|
neutral
|
|
>
|
|
{isMobile ? null : t("Edit")}
|
|
</Button>
|
|
</Tooltip>
|
|
</Action>
|
|
) : (
|
|
<div />
|
|
);
|
|
|
|
const hasSidebar = !!(sharedTree && sharedTree.children?.length);
|
|
|
|
return (
|
|
<StyledHeader
|
|
ref={measureRef}
|
|
$hidden={isEditingFocus}
|
|
title={
|
|
<Flex gap={4}>
|
|
{document.icon && (
|
|
<Icon
|
|
value={document.icon}
|
|
initial={document.initial}
|
|
color={document.color ?? undefined}
|
|
/>
|
|
)}
|
|
{document.title}
|
|
</Flex>
|
|
}
|
|
hasSidebar={hasSidebar}
|
|
left={
|
|
isMobile ? (
|
|
hasHeadings ? (
|
|
<TableOfContentsMenu />
|
|
) : null
|
|
) : hasSidebar ? (
|
|
<PublicBreadcrumb
|
|
documentId={document.id}
|
|
shareId={shareId}
|
|
sharedTree={sharedTree}
|
|
>
|
|
{hasHeadings ? toc : null}
|
|
</PublicBreadcrumb>
|
|
) : (
|
|
<Flex align="center" gap={8}>
|
|
{share && <HeaderBranding share={share} />}
|
|
{hasHeadings ? toc : null}
|
|
</Flex>
|
|
)
|
|
}
|
|
actions={
|
|
<>
|
|
{allowSubscriptions !== false && !user && env.EMAIL_ENABLED && (
|
|
<SubscribeAction shareId={shareId} documentId={document.id} />
|
|
)}
|
|
<AppearanceAction />
|
|
{can.update && share && (
|
|
<Action>
|
|
<ShareSettingsPopover share={share}>
|
|
<Button
|
|
icon={<SettingsIcon />}
|
|
aria-label={t("Display settings")}
|
|
neutral
|
|
borderOnHover
|
|
/>
|
|
</ShareSettingsPopover>
|
|
</Action>
|
|
)}
|
|
{editAction}
|
|
</>
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const StyledHeader = styled(Header)<{ $hidden: boolean }>`
|
|
transition: opacity 500ms ease-in-out;
|
|
${(props) => props.$hidden && "opacity: 0;"}
|
|
`;
|
|
|
|
export default observer(SharedDocumentHeader);
|