Compare commits

..

1 Commits

Author SHA1 Message Date
google-labs-jules[bot] 22a6a25f00 Fix: Prevent multiple context menus from being open simultaneously
This commit addresses an issue where multiple context menus could be open at the same time.

The fix involves the following:

- Modified `MenuContext` in `app/hooks/useMenuContext.tsx` to store a reference to the `hide` function of the currently open menu, instead of a boolean.
- Updated the `ContextMenu` component in `app/components/ContextMenu/index.tsx` to:
    - Use the modified `MenuContext`.
    - Before opening a new menu, check if another menu is already open.
    - If an existing menu is open, it's closed before the new one is displayed.

Testing:
- I attempted to add a test for this change. However, due to a Node.js version incompatibility (sandbox: 18.19.1 vs. required: >=18.20 for a transitive dependency `lru-cache@^11.0.0` of `jest-environment-jsdom`), I was not able to run Jest tests in the development environment.
- I recommend that you run tests in a compatible Node.js environment, such as in your CI/CD pipeline, to ensure the new behavior is correctly tested and existing functionality remains unaffected.
2025-06-01 02:36:34 +00:00
287 changed files with 2985 additions and 6621 deletions
+1 -17
View File
@@ -27,20 +27,6 @@
"eslint-plugin-lodash"
],
"rules": {
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "reakit/Menu",
"importNames": [
"useMenuState"
],
"message": "Do not use useMenuState from reakit/Menu. Use useMenuState instead."
}
]
}
],
"eqeqeq": 2,
"curly": 2,
"no-console": "error",
@@ -72,7 +58,6 @@
"ignoreTypeValueShadow": true
}
],
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/await-thenable": "error",
@@ -86,7 +71,6 @@
"error",
{
"argsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_",
"args": "after-used",
"ignoreRestSiblings": true
}
@@ -187,4 +171,4 @@
"typescript": {}
}
}
}
}
+2 -2
View File
@@ -106,8 +106,8 @@ export const startTyping = createAction({
}, 250);
window.addEventListener("keydown", (event) => {
if (event.key === "Escape" && intervalId) {
clearInterval(intervalId);
if (event.key === "Escape") {
intervalId && clearInterval(intervalId);
}
});
+4 -9
View File
@@ -750,7 +750,7 @@ export const importDocument = createAction({
return false;
},
perform: ({ activeDocumentId, activeCollectionId, stores }) => {
perform: ({ activeCollectionId, activeDocumentId, stores }) => {
const { documents } = stores;
const input = document.createElement("input");
input.type = "file";
@@ -1081,17 +1081,12 @@ export const openDocumentComments = createAction({
analyticsName: "Open comments",
section: ActiveDocumentSection,
icon: <CommentIcon />,
visible: ({ activeCollectionId, activeDocumentId, stores }) => {
visible: ({ activeDocumentId, stores }) => {
const can = stores.policies.abilities(activeDocumentId ?? "");
const collection = activeCollectionId
? stores.collections.get(activeCollectionId)
: undefined;
return (
!!activeDocumentId &&
can.comment &&
(collection?.canCreateComment ??
!!stores.auth.team?.getPreference(TeamPreference.Commenting))
!!stores.auth.team?.getPreference(TeamPreference.Commenting)
);
},
perform: ({ activeDocumentId, stores }) => {
@@ -1215,7 +1210,7 @@ export const leaveDocument = createAction({
} as UserMembership);
toast.success(t("You have left the shared document"));
} catch (_err) {
} catch (err) {
toast.error(t("Could not leave document"));
}
},
+1 -3
View File
@@ -57,15 +57,13 @@ export const createTeam = createAction({
perform: ({ t, event, stores }) => {
event?.preventDefault();
event?.stopPropagation();
const { user } = stores.auth;
if (user) {
user &&
stores.dialogs.openModal({
title: t("Create a workspace"),
fullscreen: true,
content: <TeamNew user={user} />,
});
}
},
});
+2 -4
View File
@@ -49,7 +49,7 @@ type Props = {
};
const AuthenticatedLayout: React.FC = ({ children }: Props) => {
const { ui, auth, collections } = useStores();
const { ui, auth } = useStores();
const location = useLocation();
const layoutRef = React.useRef<HTMLDivElement>(null);
const can = usePolicy(ui.activeDocumentId);
@@ -108,9 +108,7 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
can.comment &&
ui.activeDocumentId &&
ui.commentsExpanded &&
(ui.activeCollectionId
? collections.get(ui.activeCollectionId)?.canCreateComment
: !!team.getPreference(TeamPreference.Commenting));
team.getPreference(TeamPreference.Commenting);
const sidebarRight = (
<AnimatePresence
+29 -47
View File
@@ -1,4 +1,3 @@
import * as Popover from "@radix-ui/react-popover";
import filter from "lodash/filter";
import isEqual from "lodash/isEqual";
import orderBy from "lodash/orderBy";
@@ -6,16 +5,15 @@ import uniq from "lodash/uniq";
import { observer } from "mobx-react";
import { useState, useMemo, useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { depths, s } from "@shared/styles";
import { usePopoverState, PopoverDisclosure } from "reakit/Popover";
import Document from "~/models/Document";
import { AvatarSize, AvatarWithPresence } from "~/components/Avatar";
import DocumentViews from "~/components/DocumentViews";
import Facepile from "~/components/Facepile";
import NudeButton from "~/components/NudeButton";
import Popover from "~/components/Popover";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import { fadeAndScaleIn } from "~/styles/animations";
type Props = {
/** The document to display live collaborators for */
@@ -24,21 +22,6 @@ type Props = {
limit?: number;
};
// Styled components to match the original Popover styling
const StyledPopoverContent = styled(Popover.Content)`
animation: ${fadeAndScaleIn} 200ms ease;
transform-origin: 75% 0;
background: ${s("menuBackground")};
border-radius: 6px;
padding: 12px 24px;
max-height: 75vh;
box-shadow: ${s("menuShadow")};
z-index: ${depths.modal};
overflow-x: hidden;
overflow-y: auto;
outline: none;
`;
/**
* Displays a list of live collaborators for a document, including their avatars
* and presence status.
@@ -49,7 +32,6 @@ function Collaborators(props: Props) {
const user = useCurrentUser();
const currentUserId = user?.id;
const [requestedUserIds, setRequestedUserIds] = useState<string[]>([]);
const [popoverOpen, setPopoverOpen] = useState(false);
const { users, presence, ui } = useStores();
const { document } = props;
const { observingUserId } = ui;
@@ -112,6 +94,11 @@ function Collaborators(props: Props) {
}
}, [missingUserIds, requestedUserIds, users]);
const popover = usePopoverState({
gutter: 0,
placement: "bottom-end",
});
// Memoize onClick handler to avoid inline function creation
const handleAvatarClick = useCallback(
(
@@ -163,33 +150,28 @@ function Collaborators(props: Props) {
);
return (
<Popover.Root open={popoverOpen} onOpenChange={setPopoverOpen}>
<Popover.Trigger asChild>
<NudeButton
width={Math.min(collaborators.length, limit) * AvatarSize.Large}
height={AvatarSize.Large}
>
<Facepile
size={AvatarSize.Large}
limit={limit}
overflow={Math.max(0, collaborators.length - limit)}
users={collaborators}
renderAvatar={renderAvatar}
/>
</NudeButton>
</Popover.Trigger>
<Popover.Portal>
<StyledPopoverContent
side="bottom"
align="end"
sideOffset={0}
aria-label={t("Viewers")}
style={{ width: 300 }}
>
<DocumentViews document={document} />
</StyledPopoverContent>
</Popover.Portal>
</Popover.Root>
<>
<PopoverDisclosure {...popover}>
{(popoverProps) => (
<NudeButton
width={Math.min(collaborators.length, limit) * AvatarSize.Large}
height={AvatarSize.Large}
{...popoverProps}
>
<Facepile
size={AvatarSize.Large}
limit={limit}
overflow={Math.max(0, collaborators.length - limit)}
users={collaborators}
renderAvatar={renderAvatar}
/>
</NudeButton>
)}
</PopoverDisclosure>
<Popover {...popover} width={300} aria-label={t("Viewers")} tabIndex={0}>
{popover.visible && <DocumentViews document={document} />}
</Popover>
</>
);
}
+4 -15
View File
@@ -6,7 +6,7 @@ import { Trans, useTranslation } from "react-i18next";
import styled from "styled-components";
import Icon from "@shared/components/Icon";
import { randomElement } from "@shared/random";
import { CollectionPermission, TeamPreference } from "@shared/types";
import { CollectionPermission } from "@shared/types";
import { IconLibrary } from "@shared/utils/IconLibrary";
import { colorPalette } from "@shared/utils/collections";
import { CollectionValidation } from "@shared/validations";
@@ -22,7 +22,6 @@ import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
import { EmptySelectValue } from "~/types";
import { createSwitchRegister } from "~/utils/forms";
const IconPicker = createLazyComponent(() => import("~/components/IconPicker"));
@@ -32,7 +31,6 @@ export interface FormData {
color: string | null;
sharing: boolean;
permission: CollectionPermission | undefined;
commenting?: boolean | null;
}
const useIconColor = (collection?: Collection) => {
@@ -85,7 +83,6 @@ export const CollectionForm = observer(function CollectionForm_({
icon: collection?.icon,
sharing: collection?.sharing ?? true,
permission: collection?.permission,
commenting: collection?.commenting ?? true,
color: iconColor,
},
});
@@ -115,7 +112,7 @@ export const CollectionForm = observer(function CollectionForm_({
}, [setFocus]);
const handleIconChange = useCallback(
(icon: string, color: string) => {
(icon: string, color: string | null) => {
if (icon !== values.icon) {
setFocus("name");
}
@@ -132,6 +129,7 @@ export const CollectionForm = observer(function CollectionForm_({
<Trans>
Collections are used to group documents and choose permissions
</Trans>
.
</Text>
<Flex gap={8}>
<Input
@@ -188,16 +186,7 @@ export const CollectionForm = observer(function CollectionForm_({
note={t(
"Allow documents within this collection to be shared publicly on the internet."
)}
{...createSwitchRegister(register, "sharing")}
/>
)}
{team.getPreference(TeamPreference.Commenting) && (
<Switch
id="commenting"
label={t("Commenting")}
note={t("Allow commenting on documents within this collection.")}
{...createSwitchRegister(register, "commenting")}
{...register("sharing")}
/>
)}
+2 -4
View File
@@ -3,6 +3,7 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import {
useMenuState,
MenuButton,
MenuItem as BaseMenuItem,
MenuStateReturn,
@@ -12,7 +13,6 @@ import MenuIconWrapper from "~/components/ContextMenu/MenuIconWrapper";
import Flex from "~/components/Flex";
import { actionToMenuItem } from "~/actions";
import useActionContext from "~/hooks/useActionContext";
import { useMenuState } from "~/hooks/useMenuState";
import {
Action,
ActionContext,
@@ -52,9 +52,7 @@ const SubMenu = React.forwardRef(function _Template(
) {
const { t } = useTranslation();
const theme = useTheme();
const menu = useMenuState({
parentId: parentMenuState.baseId,
});
const menu = useMenuState();
return (
<>
+10 -13
View File
@@ -67,28 +67,25 @@ const ContextMenu: React.FC<Props> = ({
const previousVisible = usePrevious(rest.visible);
const { ui } = useStores();
const { t } = useTranslation();
const { setIsMenuOpen } = useMenuContext();
const { openMenuHideFn, setOpenMenuHideFn } = useMenuContext();
const isMobile = useMobile();
const isSubMenu = !!parentMenuState;
useUnmount(() => {
setIsMenuOpen(false);
});
React.useEffect(() => {
if (rest.visible && !previousVisible) {
onOpen?.();
if (!isSubMenu) {
setIsMenuOpen(true);
if (openMenuHideFn && openMenuHideFn !== rest.hide) {
openMenuHideFn();
}
setOpenMenuHideFn(() => rest.hide);
}
if (!rest.visible && previousVisible) {
onClose?.();
if (!isSubMenu) {
setIsMenuOpen(false);
if (openMenuHideFn === rest.hide) {
setOpenMenuHideFn(null);
}
}
}, [
@@ -97,9 +94,11 @@ const ContextMenu: React.FC<Props> = ({
previousVisible,
rest.visible,
ui.sidebarCollapsed,
setIsMenuOpen,
isSubMenu,
t,
openMenuHideFn,
setOpenMenuHideFn,
rest.hide,
]);
// Perf win don't render anything until the menu has been opened
@@ -171,9 +170,7 @@ const InnerContextMenu = (props: InnerContextMenuProps) => {
});
}
return () => {
if (scrollElement && !props.isSubMenu) {
enableBodyScroll(scrollElement);
}
scrollElement && !props.isSubMenu && enableBodyScroll(scrollElement);
};
}, [props.isSubMenu, props.visible]);
+3 -7
View File
@@ -15,7 +15,7 @@ function CopyToClipboard(props: Props, ref: React.Ref<HTMLElement>) {
const onClick = React.useCallback(
(ev: React.MouseEvent<HTMLElement>) => {
const childElem = React.Children.only(children);
const elem = React.Children.only(children);
copy(text, {
debug: env.ENVIRONMENT !== "production",
@@ -24,12 +24,8 @@ function CopyToClipboard(props: Props, ref: React.Ref<HTMLElement>) {
onCopy?.();
if (
childElem &&
childElem.props &&
typeof childElem.props.onClick === "function"
) {
childElem.props.onClick(ev);
if (elem && elem.props && typeof elem.props.onClick === "function") {
elem.props.onClick(ev);
} else {
ev.preventDefault();
ev.stopPropagation();
+17 -3
View File
@@ -46,6 +46,20 @@ function DocumentCopy({ document, onSubmit }: Props) {
return nodes;
}, [policies, collectionTrees, document.isTemplate]);
const handlePublishChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setPublish(ev.target.checked);
},
[]
);
const handleRecursiveChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setRecursive(ev.target.checked);
},
[]
);
const copy = async () => {
if (!selectedPath) {
toast.message(t("Select a location to copy"));
@@ -65,7 +79,7 @@ function DocumentCopy({ document, onSubmit }: Props) {
toast.success(t("Document copied"));
onSubmit(result);
} catch (_err) {
} catch (err) {
toast.error(t("Couldnt copy the document, try again?"));
}
};
@@ -88,7 +102,7 @@ function DocumentCopy({ document, onSubmit }: Props) {
label={t("Publish")}
labelPosition="right"
checked={publish}
onChange={setPublish}
onChange={handlePublishChange}
/>
</Text>
)}
@@ -99,7 +113,7 @@ function DocumentCopy({ document, onSubmit }: Props) {
label={t("Include nested documents")}
labelPosition="right"
checked={recursive}
onChange={setRecursive}
onChange={handleRecursiveChange}
/>
</Text>
)}
+4 -4
View File
@@ -60,7 +60,7 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
if (defaultValue) {
const node = items.find((item) => item.id === defaultValue);
if (node) {
return ancestors(node).map((ancestorNode) => ancestorNode.id);
return ancestors(node).map((node) => node.id);
}
}
return [];
@@ -99,10 +99,10 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
}, [searchTerm]);
React.useEffect(() => {
setItemRefs((existingItemRefs) =>
setItemRefs((itemRefs) =>
map(
fill(Array(items.length), 0),
(_, i) => existingItemRefs[i] || React.createRef()
(_, i) => itemRefs[i] || React.createRef()
)
);
}, [items.length]);
@@ -180,7 +180,7 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
);
// remove children
const newNodes = filter(nodes, (n) => !includes(descendantIds, n.id));
const newNodes = filter(nodes, (node) => !includes(descendantIds, node.id));
const scrollOffset = calculateInitialScrollOffset(newNodes.length);
setInitialScrollOffset(scrollOffset);
};
+1 -2
View File
@@ -1,7 +1,7 @@
import deburr from "lodash/deburr";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { MenuButton } from "reakit/Menu";
import { useMenuState, MenuButton } from "reakit/Menu";
import styled from "styled-components";
import { s } from "@shared/styles";
import type { FetchPageParams } from "~/stores/base/Store";
@@ -9,7 +9,6 @@ import Button, { Inner } from "~/components/Button";
import ContextMenu from "~/components/ContextMenu";
import MenuItem from "~/components/ContextMenu/MenuItem";
import Text from "~/components/Text";
import { useMenuState } from "~/hooks/useMenuState";
import Input, { NativeInput, Outline } from "./Input";
import PaginatedList, { PaginatedItem } from "./PaginatedList";
+4 -4
View File
@@ -36,8 +36,8 @@ const Guide: React.FC<Props> = ({
return (
<DialogBackdrop {...dialog}>
{(backdropProps) => (
<Backdrop {...backdropProps}>
{(props) => (
<Backdrop {...props}>
<Dialog
{...dialog}
aria-label={title}
@@ -45,8 +45,8 @@ const Guide: React.FC<Props> = ({
hideOnEsc
hide={onRequestClose}
>
{(dialogProps) => (
<Scene {...dialogProps} {...rest}>
{(props) => (
<Scene {...props} {...rest}>
<Content>
{title && <Header>{title}</Header>}
{children}
@@ -193,7 +193,7 @@ const SwitcherButton = styled(NudeButton)<{ panel: Panel }>`
`;
const LargeMobileBuiltinColors = styled(BuiltinColors)`
max-width: 400px;
max-width: 380px;
padding-right: 8px;
`;
@@ -1,6 +1,6 @@
import { useMemo, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { Menu, MenuButton, MenuItem } from "reakit";
import { Menu, MenuButton, MenuItem, useMenuState } from "reakit";
import styled from "styled-components";
import { depths, s, hover } from "@shared/styles";
import { EmojiSkinTone } from "@shared/types";
@@ -8,7 +8,6 @@ import { getEmojiVariants } from "@shared/utils/emoji";
import { Emoji } from "~/components/Emoji";
import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import { useMenuState } from "~/hooks/useMenuState";
import { IconButton } from "./IconButton";
const SkinTonePicker = ({
+138 -196
View File
@@ -1,20 +1,27 @@
import * as Popover from "@radix-ui/react-popover";
import * as Tabs from "@radix-ui/react-tabs";
import { SmileyIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import {
PopoverDisclosure,
Tab,
TabList,
TabPanel,
usePopoverState,
useTabState,
} from "reakit";
import styled, { css } from "styled-components";
import Icon from "@shared/components/Icon";
import { s, hover, depths } from "@shared/styles";
import { s, hover } from "@shared/styles";
import theme from "@shared/styles/theme";
import { IconType } from "@shared/types";
import { determineIconType } from "@shared/utils/icon";
import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import Popover from "~/components/Popover";
import useMobile from "~/hooks/useMobile";
import useOnClickOutside from "~/hooks/useOnClickOutside";
import usePrevious from "~/hooks/usePrevious";
import useWindowSize from "~/hooks/useWindowSize";
import { fadeAndScaleIn } from "~/styles/animations";
import { Drawer, DrawerContent, DrawerTrigger } from "../primitives/Drawer";
import EmojiPanel from "./components/EmojiPanel";
import IconPanel from "./components/IconPanel";
import { PopoverButton } from "./components/PopoverButton";
@@ -24,8 +31,6 @@ const TAB_NAMES = {
Emoji: "emoji",
} as const;
type TabName = (typeof TAB_NAMES)[keyof typeof TAB_NAMES];
const POPOVER_WIDTH = 408;
type Props = {
@@ -62,9 +67,9 @@ const IconPicker = ({
const { width: windowWidth } = useWindowSize();
const isMobile = useMobile();
const [open, setOpen] = React.useState(false);
const [query, setQuery] = React.useState("");
const [chosenColor, setChosenColor] = React.useState(color);
const contentRef = React.useRef<HTMLDivElement | null>(null);
const iconType = determineIconType(icon);
const defaultTab = React.useMemo(
@@ -73,40 +78,32 @@ const IconPicker = ({
[iconType]
);
const [activeTab, setActiveTab] = React.useState<TabName>(defaultTab);
const popover = usePopoverState({
placement: popoverPosition,
modal: true,
unstable_offset: [0, 0],
});
const { hide, show, visible } = popover;
const tab = useTabState({ selectedId: defaultTab });
const previouslyVisible = usePrevious(popover.visible);
const popoverWidth = isMobile ? windowWidth : POPOVER_WIDTH;
const handleTabChange = React.useCallback((value: string) => {
setActiveTab(value as TabName);
}, []);
// In mobile, popover is absolutely positioned to leave 8px on both sides.
const panelWidth = isMobile ? windowWidth - 16 : popoverWidth;
const resetDefaultTab = React.useCallback(() => {
setActiveTab(defaultTab);
tab.select(defaultTab);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultTab]);
const handleOpenChange = React.useCallback(
(isOpen: boolean) => {
setOpen(isOpen);
if (isOpen) {
onOpen?.();
} else {
onClose?.();
setQuery("");
resetDefaultTab();
}
},
[onOpen, onClose, resetDefaultTab]
);
const handleIconChange = React.useCallback(
(ic: string) => {
setOpen(false);
hide();
const icType = determineIconType(ic);
const finalColor = icType === IconType.SVG ? chosenColor : null;
onChange(ic, finalColor);
},
[onChange, chosenColor]
[hide, onChange, chosenColor]
);
const handleIconColorChange = React.useCallback(
@@ -114,6 +111,7 @@ const IconPicker = ({
setChosenColor(c);
const icType = determineIconType(icon);
// Outline icon set; propagate color change
if (icType === IconType.SVG) {
onChange(icon, c);
}
@@ -122,40 +120,60 @@ const IconPicker = ({
);
const handleIconRemove = React.useCallback(() => {
setOpen(false);
hide();
onChange(null, null);
}, [setOpen, onChange]);
}, [hide, onChange]);
const PickerContent = (
<Content
open={open}
activeTab={activeTab}
iconColor={chosenColor}
iconInitial={initial ?? ""}
query={query}
panelWidth={popoverWidth}
allowDelete={!!(allowDelete && icon)}
onTabChange={handleTabChange}
onQueryChange={setQuery}
onIconChange={handleIconChange}
onIconColorChange={handleIconColorChange}
onIconRemove={handleIconRemove}
/>
const handlePopoverButtonClick = React.useCallback(
(ev: React.MouseEvent) => {
ev.stopPropagation();
if (visible) {
hide();
} else {
show();
}
},
[hide, show, visible]
);
// Update selected tab when default tab changes
// Popover open effect
React.useEffect(() => {
setActiveTab(defaultTab);
}, [defaultTab]);
if (visible && !previouslyVisible) {
onOpen?.();
} else if (!visible && previouslyVisible) {
onClose?.();
setQuery("");
resetDefaultTab();
}
}, [visible, previouslyVisible, onOpen, onClose, resetDefaultTab]);
if (isMobile) {
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
// Custom click outside handling rather than using `hideOnClickOutside` from reakit so that we can
// prevent event bubbling.
useOnClickOutside(
contentRef,
(event) => {
if (
popover.visible &&
!popover.unstable_disclosureRef.current?.contains(event.target as Node)
) {
event.stopPropagation();
event.preventDefault();
popover.hide();
}
},
{ capture: true }
);
return (
<>
<PopoverDisclosure {...popover}>
{(props) => (
<PopoverButton
{...props}
aria-label={t("Show menu")}
className={className}
size={size}
onClick={handlePopoverButtonClick}
$borderOnHover={borderOnHover}
>
{children ? (
@@ -166,124 +184,71 @@ const IconPicker = ({
<StyledSmileyIcon color={theme.placeholder} size={size} />
)}
</PopoverButton>
</DrawerTrigger>
<DrawerContent aria-label={t("Icon Picker")}>
{PickerContent}
</DrawerContent>
</Drawer>
);
}
return (
<Popover.Root open={open} onOpenChange={handleOpenChange} modal={true}>
<Popover.Trigger asChild>
<PopoverButton
aria-label={t("Show menu")}
className={className}
size={size}
$borderOnHover={borderOnHover}
>
{children ? (
children
) : iconType && icon ? (
<Icon value={icon} color={color} size={size} initial={initial} />
) : (
<StyledSmileyIcon color={theme.placeholder} size={size} />
)}
</PopoverButton>
</Popover.Trigger>
<Popover.Portal>
<StyledPopoverContent
side={popoverPosition === "right" ? "right" : "bottom"}
align={popoverPosition === "bottom-start" ? "start" : "center"}
sideOffset={0}
width={popoverWidth}
aria-label={t("Icon Picker")}
onClick={(e) => e.stopPropagation()}
>
{PickerContent}
</StyledPopoverContent>
</Popover.Portal>
</Popover.Root>
);
};
type ContentProps = {
open: boolean;
activeTab: TabName;
query: string;
iconColor: string;
iconInitial: string;
panelWidth: number;
allowDelete: boolean;
onTabChange: (tab: string) => void;
onQueryChange: (query: string) => void;
onIconChange: (icon: string) => void;
onIconColorChange: (color: string) => void;
onIconRemove: () => void;
};
const Content = ({
open,
activeTab,
iconColor,
iconInitial,
query,
panelWidth,
allowDelete,
onTabChange,
onQueryChange,
onIconChange,
onIconColorChange,
onIconRemove,
}: ContentProps) => {
const { t } = useTranslation();
return (
<Tabs.Root value={activeTab} onValueChange={onTabChange}>
<TabActionsWrapper justify="space-between" align="center">
<Tabs.List>
<StyledTab
value={TAB_NAMES["Icon"]}
aria-label={t("Icons")}
$active={activeTab === TAB_NAMES["Icon"]}
>
{t("Icons")}
</StyledTab>
<StyledTab
value={TAB_NAMES["Emoji"]}
aria-label={t("Emojis")}
$active={activeTab === TAB_NAMES["Emoji"]}
>
{t("Emojis")}
</StyledTab>
</Tabs.List>
{allowDelete && (
<RemoveButton onClick={onIconRemove}>{t("Remove")}</RemoveButton>
)}
</TabActionsWrapper>
<StyledTabContent value={TAB_NAMES["Icon"]}>
<IconPanel
panelWidth={panelWidth}
initial={iconInitial}
color={iconColor}
query={query}
panelActive={open && activeTab === TAB_NAMES["Icon"]}
onIconChange={onIconChange}
onColorChange={onIconColorChange}
onQueryChange={onQueryChange}
/>
</StyledTabContent>
<StyledTabContent value={TAB_NAMES["Emoji"]}>
<EmojiPanel
panelWidth={panelWidth}
query={query}
panelActive={open && activeTab === TAB_NAMES["Emoji"]}
onEmojiChange={onIconChange}
onQueryChange={onQueryChange}
/>
</StyledTabContent>
</Tabs.Root>
</PopoverDisclosure>
<Popover
{...popover}
ref={contentRef}
width={popoverWidth}
shrink
aria-label={t("Icon Picker")}
onClick={(e) => e.stopPropagation()}
hideOnClickOutside={false}
>
<>
<TabActionsWrapper justify="space-between" align="center">
<TabList {...tab}>
<StyledTab
{...tab}
id={TAB_NAMES["Icon"]}
aria-label={t("Icons")}
$active={tab.selectedId === TAB_NAMES["Icon"]}
>
{t("Icons")}
</StyledTab>
<StyledTab
{...tab}
id={TAB_NAMES["Emoji"]}
aria-label={t("Emojis")}
$active={tab.selectedId === TAB_NAMES["Emoji"]}
>
{t("Emojis")}
</StyledTab>
</TabList>
{allowDelete && icon && (
<RemoveButton onClick={handleIconRemove}>
{t("Remove")}
</RemoveButton>
)}
</TabActionsWrapper>
<StyledTabPanel {...tab}>
<IconPanel
panelWidth={panelWidth}
initial={initial ?? "?"}
color={chosenColor}
query={query}
panelActive={
popover.visible && tab.selectedId === TAB_NAMES["Icon"]
}
onIconChange={handleIconChange}
onColorChange={handleIconColorChange}
onQueryChange={setQuery}
/>
</StyledTabPanel>
<StyledTabPanel {...tab}>
<EmojiPanel
panelWidth={panelWidth}
query={query}
panelActive={
popover.visible && tab.selectedId === TAB_NAMES["Emoji"]
}
onEmojiChange={handleIconChange}
onQueryChange={setQuery}
/>
</StyledTabPanel>
</>
</Popover>
</>
);
};
@@ -312,7 +277,7 @@ const TabActionsWrapper = styled(Flex)`
border-bottom: 1px solid ${s("inputBorder")};
`;
const StyledTab = styled(Tabs.Trigger)<{ $active: boolean }>`
const StyledTab = styled(Tab)<{ $active: boolean }>`
position: relative;
font-weight: 500;
font-size: 14px;
@@ -343,32 +308,9 @@ const StyledTab = styled(Tabs.Trigger)<{ $active: boolean }>`
`}
`;
const StyledTabContent = styled(Tabs.Content)`
const StyledTabPanel = styled(TabPanel)`
height: 410px;
overflow-y: auto;
`;
const StyledPopoverContent = styled(Popover.Content)<{ width: number }>`
animation: ${fadeAndScaleIn} 200ms ease;
transform-origin: var(--radix-popover-content-transform-origin);
background: ${s("menuBackground")};
border-radius: 6px;
padding: 6px 0;
max-height: 75vh;
box-shadow: ${s("menuShadow")};
z-index: ${depths.modal};
width: ${(props) => props.width}px;
overflow: hidden;
outline: none;
@media (max-width: 768px) {
position: fixed;
z-index: ${depths.menu};
top: 50px;
left: 8px;
right: 8px;
width: auto;
}
`;
export default React.memo(IconPicker);
+2 -2
View File
@@ -1,6 +1,6 @@
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
import * as React from "react";
import { mergeRefs } from "react-merge-refs";
import { VisuallyHidden } from "reakit/VisuallyHidden";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { s, ellipsis } from "@shared/styles";
@@ -221,7 +221,7 @@ function Input(
<label>
{label &&
(labelHidden ? (
<VisuallyHidden.Root>{wrappedLabel}</VisuallyHidden.Root>
<VisuallyHidden>{wrappedLabel}</VisuallyHidden>
) : (
wrappedLabel
))}
+1 -2
View File
@@ -1,9 +1,8 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import { MenuButton } from "reakit/Menu";
import { MenuButton, useMenuState } from "reakit/Menu";
import styled from "styled-components";
import { s } from "@shared/styles";
import { useMenuState } from "~/hooks/useMenuState";
import lazyWithRetry from "~/utils/lazyWithRetry";
import ContextMenu from "./ContextMenu";
import DelayedMount from "./DelayedMount";
+2 -2
View File
@@ -1,4 +1,3 @@
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
import {
Select,
SelectOption,
@@ -8,6 +7,7 @@ import {
} from "@renderlesskit/react";
import { CheckmarkIcon } from "outline-icons";
import * as React from "react";
import { VisuallyHidden } from "reakit/VisuallyHidden";
import styled, { css } from "styled-components";
import { s } from "@shared/styles";
import Button, { Props as ButtonProps, Inner } from "~/components/Button";
@@ -213,7 +213,7 @@ const InputSelect = (props: Props, ref: React.RefObject<InputSelectRef>) => {
<Wrapper short={short}>
{label &&
(labelHidden ? (
<VisuallyHidden.Root>{wrappedLabel}</VisuallyHidden.Root>
<VisuallyHidden>{wrappedLabel}</VisuallyHidden>
) : (
wrappedLabel
))}
+27 -29
View File
@@ -38,41 +38,39 @@ const Layout = React.forwardRef(function Layout_(
});
return (
<MenuProvider>
<Container column auto ref={ref}>
<Helmet>
<title>{title ? title : env.APP_NAME}</title>
</Helmet>
<Container column auto ref={ref}>
<Helmet>
<title>{title ? title : env.APP_NAME}</title>
</Helmet>
<SkipNavLink />
<SkipNavLink />
{ui.progressBarVisible && <LoadingIndicatorBar />}
{ui.progressBarVisible && <LoadingIndicatorBar />}
<Container auto>
<MenuProvider>{sidebar}</MenuProvider>
<Container auto>
<MenuProvider>{sidebar}</MenuProvider>
<SkipNavContent />
<Content
auto
justify="center"
$isResizing={ui.sidebarIsResizing}
$sidebarCollapsed={sidebarCollapsed}
$hasSidebar={!!sidebar}
style={
sidebarCollapsed
? undefined
: {
marginLeft: `${ui.sidebarWidth}px`,
}
}
>
{children}
</Content>
<SkipNavContent />
<Content
auto
justify="center"
$isResizing={ui.sidebarIsResizing}
$sidebarCollapsed={sidebarCollapsed}
$hasSidebar={!!sidebar}
style={
sidebarCollapsed
? undefined
: {
marginLeft: `${ui.sidebarWidth}px`,
}
}
>
{children}
</Content>
{sidebarRight}
</Container>
{sidebarRight}
</Container>
</MenuProvider>
</Container>
);
});
+3 -3
View File
@@ -33,7 +33,7 @@ export type Props = Omit<React.HTMLAttributes<HTMLAnchorElement>, "title"> & {
small?: boolean;
/** Whether to enable keyboard navigation */
keyboardNavigation?: boolean;
enableEllipsis?: boolean;
ellipsis?: boolean;
};
const ListItem = (
@@ -46,7 +46,7 @@ const ListItem = (
border,
to,
keyboardNavigation,
enableEllipsis,
ellipsis,
...rest
}: Props,
ref: React.RefObject<HTMLAnchorElement>
@@ -85,7 +85,7 @@ const ListItem = (
column={!compact}
$selected={selected}
>
<Heading $small={small} $ellipsis={enableEllipsis}>
<Heading $small={small} $ellipsis={ellipsis}>
{title}
</Heading>
{subtitle && (
+75 -80
View File
@@ -1,9 +1,9 @@
import * as Dialog from "@radix-ui/react-dialog";
import { observer } from "mobx-react";
import { CloseIcon, BackIcon } from "outline-icons";
import { transparentize } from "polished";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Dialog, DialogBackdrop, useDialogState } from "reakit/Dialog";
import styled, { DefaultTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { depths, s } from "@shared/styles";
@@ -37,6 +37,9 @@ const Modal: React.FC<Props> = ({
style,
onRequestClose,
}: Props) => {
const dialog = useDialogState({
animated: 250,
});
const [depth, setDepth] = React.useState(0);
const wasOpen = usePrevious(isOpen);
const isMobile = useMobile();
@@ -45,12 +48,14 @@ const Modal: React.FC<Props> = ({
React.useEffect(() => {
if (!wasOpen && isOpen) {
setDepth(openModals++);
dialog.show();
}
if (wasOpen && !isOpen) {
setDepth(openModals--);
dialog.hide();
}
}, [wasOpen, isOpen]);
}, [dialog, wasOpen, isOpen]);
useUnmount(() => {
if (isOpen) {
@@ -63,75 +68,78 @@ const Modal: React.FC<Props> = ({
}
return (
<Dialog.Root
open={isOpen}
onOpenChange={(open) => !open && onRequestClose()}
>
<Dialog.Portal>
<StyledOverlay $fullscreen={fullscreen}>
<StyledContent
onEscapeKeyDown={onRequestClose}
onPointerDownOutside={fullscreen ? undefined : onRequestClose}
aria-describedby={undefined}
<DialogBackdrop {...dialog}>
{(props) => (
<Backdrop $fullscreen={fullscreen} {...props}>
<Dialog
{...dialog}
aria-label={typeof title === "string" ? title : undefined}
preventBodyScroll
hideOnEsc
hideOnClickOutside={!fullscreen}
hide={onRequestClose}
>
{fullscreen || isMobile ? (
<Fullscreen
$nested={!!depth}
style={
isMobile
? undefined
: {
marginLeft: `${depth * 12}px`,
}
}
>
<Content>
<Centered onClick={(ev) => ev.stopPropagation()} column>
{title && (
<Text size="xlarge" weight="bold">
{title}
</Text>
)}
<ErrorBoundary>{children}</ErrorBoundary>
</Centered>
</Content>
<Close onClick={onRequestClose}>
<CloseIcon size={32} />
</Close>
<Back onClick={onRequestClose}>
<BackIcon size={32} />
<Text>{t("Back")} </Text>
</Back>
</Fullscreen>
) : (
<Small>
<Centered
onClick={(ev) => ev.stopPropagation()}
// maxHeight needed for proper overflow behavior in Safari
style={{ maxHeight: "65vh" }}
column
reverse
{(props) =>
fullscreen || isMobile ? (
<Fullscreen
$nested={!!depth}
style={
isMobile
? undefined
: {
marginLeft: `${depth * 12}px`,
}
}
{...props}
>
<SmallContent style={style} shadow>
<ErrorBoundary component="div">{children}</ErrorBoundary>
</SmallContent>
<Header>
{title && <Text size="large">{title}</Text>}
<NudeButton onClick={onRequestClose}>
<CloseIcon />
</NudeButton>
</Header>
</Centered>
</Small>
)}
</StyledContent>
</StyledOverlay>
</Dialog.Portal>
</Dialog.Root>
<Content>
<Centered onClick={(ev) => ev.stopPropagation()} column>
{title && (
<Text size="xlarge" weight="bold">
{title}
</Text>
)}
<ErrorBoundary>{children}</ErrorBoundary>
</Centered>
</Content>
<Close onClick={onRequestClose}>
<CloseIcon size={32} />
</Close>
<Back onClick={onRequestClose}>
<BackIcon size={32} />
<Text>{t("Back")} </Text>
</Back>
</Fullscreen>
) : (
<Small {...props}>
<Centered
onClick={(ev) => ev.stopPropagation()}
// maxHeight needed for proper overflow behavior in Safari
style={{ maxHeight: "65vh" }}
column
reverse
>
<SmallContent style={style} shadow>
<ErrorBoundary component="div">{children}</ErrorBoundary>
</SmallContent>
<Header>
{title && <Text size="large">{title}</Text>}
<NudeButton onClick={onRequestClose}>
<CloseIcon />
</NudeButton>
</Header>
</Centered>
</Small>
)
}
</Dialog>
</Backdrop>
)}
</DialogBackdrop>
);
};
const StyledOverlay = styled(Dialog.Overlay)<{ $fullscreen?: boolean }>`
const Backdrop = styled(Flex)<{ $fullscreen?: boolean }>`
position: fixed;
top: 0;
left: 0;
@@ -145,24 +153,11 @@ const StyledOverlay = styled(Dialog.Overlay)<{ $fullscreen?: boolean }>`
transition: opacity 50ms ease-in-out;
opacity: 0;
&[data-state="open"] {
&[data-enter] {
opacity: 1;
}
`;
const StyledContent = styled(Dialog.Content)`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: ${depths.modal};
display: flex;
justify-content: center;
align-items: flex-start;
outline: none;
`;
type FullscreenProps = {
$nested: boolean;
theme: DefaultTheme;
@@ -23,13 +23,15 @@ import NotificationListItem from "./NotificationListItem";
type Props = {
/** Callback when the notification panel wants to close. */
onRequestClose: () => void;
/** Whether the panel is open or not. */
isOpen: boolean;
};
/**
* A panel containing a list of notifications and controls to manage them.
*/
function Notifications(
{ onRequestClose }: Props,
{ onRequestClose, isOpen }: Props,
ref: React.RefObject<HTMLDivElement>
) {
const context = useActionContext();
@@ -80,7 +82,7 @@ function Notifications(
<PaginatedList<Notification>
fetch={notifications.fetchPage}
options={{ archived: false }}
items={notifications.orderedData}
items={isOpen ? notifications.orderedData : undefined}
renderItem={(item) => (
<NotificationListItem
key={item.id}
@@ -1,11 +1,10 @@
import * as Popover from "@radix-ui/react-popover";
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { usePopoverState, PopoverDisclosure } from "reakit/Popover";
import styled from "styled-components";
import { depths, s } from "@shared/styles";
import { fadeAndSlideUp } from "~/styles/animations";
import { depths } from "@shared/styles";
import Popover from "~/components/Popover";
import Notifications from "./Notifications";
type Props = {
@@ -15,71 +14,44 @@ type Props = {
const NotificationsPopover: React.FC = ({ children }: Props) => {
const { t } = useTranslation();
const scrollableRef = React.useRef<HTMLDivElement>(null);
const closeRef = React.useRef<HTMLButtonElement>(null);
const handleRequestClose = React.useCallback(() => {
if (closeRef.current) {
closeRef.current.click();
}
}, []);
const popover = usePopoverState({
gutter: 0,
placement: "top-start",
unstable_fixed: true,
});
const handleAutoFocus = React.useCallback((event: Event) => {
// Prevent focus from moving to the popover content
event.preventDefault();
// Reset scroll position to the top when popover is opened
if (scrollableRef.current) {
// Reset scroll position to the top when popover is opened
React.useEffect(() => {
if (popover.visible && scrollableRef.current) {
scrollableRef.current.scrollTop = 0;
scrollableRef.current.focus();
}
}, []);
}, [popover.visible]);
return (
<Popover.Root>
<Popover.Trigger asChild>{children}</Popover.Trigger>
<Popover.Portal>
<StyledContent
side="top"
align="start"
sideOffset={0}
avoidCollisions={true}
aria-label={t("Notifications")}
onOpenAutoFocus={handleAutoFocus}
>
<Notifications
onRequestClose={handleRequestClose}
ref={scrollableRef}
/>
<VisuallyHidden>
<Popover.Close ref={closeRef} />
</VisuallyHidden>
</StyledContent>
</Popover.Portal>
</Popover.Root>
<>
<PopoverDisclosure {...popover}>{children}</PopoverDisclosure>
<StyledPopover
{...popover}
scrollable={false}
mobilePosition="bottom"
aria-label={t("Notifications")}
unstable_initialFocusRef={scrollableRef}
shrink
flex
>
<Notifications
onRequestClose={popover.hide}
isOpen={popover.visible}
ref={scrollableRef}
/>
</StyledPopover>
</>
);
};
const StyledContent = styled(Popover.Content)`
const StyledPopover = styled(Popover)`
z-index: ${depths.menu};
display: flex;
animation: ${fadeAndSlideUp} 200ms ease;
transform-origin: 75% 0;
background: ${s("menuBackground")};
border-radius: 6px;
padding: 6px 0;
max-height: 75vh;
box-shadow: ${s("menuShadow")};
width: 380px;
overflow: hidden;
@media (max-width: 768px) {
position: fixed;
z-index: ${depths.menu};
top: 50px;
left: 8px;
right: 8px;
width: auto;
}
`;
export default observer(NotificationsPopover);
@@ -8,7 +8,6 @@ import ImageInput from "~/scenes/Settings/components/ImageInput";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Input, { LabelText } from "~/components/Input";
import { createSwitchRegister } from "~/utils/forms";
import isCloudHosted from "~/utils/isCloudHosted";
import Switch from "../Switch";
@@ -117,7 +116,7 @@ export const OAuthClientForm = observer(function OAuthClientForm_({
/>
{isCloudHosted && (
<Switch
{...createSwitchRegister(register, "published")}
{...register("published")}
label={t("Published")}
note={t("Allow this app to be installed by other workspaces")}
/>
+3 -5
View File
@@ -107,11 +107,9 @@ const Reaction: React.FC<Props> = ({
const handleClick = React.useCallback(
(event: React.SyntheticEvent<HTMLButtonElement>) => {
event.stopPropagation();
if (active) {
void onRemoveReaction(reaction.emoji);
} else {
void onAddReaction(reaction.emoji);
}
active
? void onRemoveReaction(reaction.emoji)
: void onAddReaction(reaction.emoji);
},
[reaction, active, onAddReaction, onRemoveReaction]
);
+1 -1
View File
@@ -41,7 +41,7 @@ const ReactionList: React.FC<Props> = ({
const loadReactedUsersData = async () => {
try {
await model.loadReactedUsersData();
} catch (_err) {
} catch (err) {
Logger.warn("Could not prefetch reaction data");
}
};
+1 -1
View File
@@ -99,7 +99,7 @@ const ReactionPicker: React.FC<Props> = ({
<>
<PopoverDisclosure {...popover}>
{(props) => (
<Tooltip content={t("Add reaction")} placement="top">
<Tooltip content={t("Add reaction")} placement="top" hideOnClick>
<NudeButton
{...props}
aria-label={t("Reaction picker")}
@@ -29,7 +29,7 @@ const ViewReactionsDialog: React.FC<Props> = ({ model }) => {
const loadReactedUsersData = async () => {
try {
await model.loadReactedUsersData();
} catch (_err) {
} catch (err) {
toast.error(t("Could not load reactions"));
}
};
+3 -3
View File
@@ -53,10 +53,10 @@ function SearchPopover({ shareId, className }: Props) {
}, [searchResults, query, show]);
const performSearch = React.useCallback(
async ({ query: searchQuery, ...options }) => {
if (searchQuery?.length > 0) {
async ({ query, ...options }) => {
if (query?.length > 0) {
const response = await documents.search({
query: searchQuery,
query,
shareId,
...options,
});
@@ -12,7 +12,6 @@ import type Collection from "~/models/Collection";
import type Document from "~/models/Document";
import Share from "~/models/Share";
import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import Scrollable from "~/components/Scrollable";
import Text from "~/components/Text";
import useCurrentTeam from "~/hooks/useCurrentTeam";
@@ -231,9 +230,7 @@ const AccessTooltip = ({
{children}
</Text>
<Tooltip content={content ?? t("Access inherited from collection")}>
<NudeButton size={18}>
<QuestionMarkIcon size={18} />
</NudeButton>
<QuestionMarkIcon size={18} />
</Tooltip>
</Flex>
);
@@ -54,7 +54,7 @@ function DocumentMembersList({ document, invitedInSession }: Props) {
})
);
}
} catch (_err) {
} catch (err) {
toast.error(t("Could not remove user"));
}
},
@@ -62,19 +62,19 @@ function DocumentMembersList({ document, invitedInSession }: Props) {
);
const handleUpdateUser = React.useCallback(
async (userToUpdate, permission) => {
async (user, permission) => {
try {
await userMemberships.create({
documentId: document.id,
userId: userToUpdate.id,
userId: user.id,
permission,
});
toast.success(
t(`Permissions for {{ userName }} updated`, {
userName: userToUpdate.name,
userName: user.name,
})
);
} catch (_err) {
} catch (err) {
toast.error(t("Could not update user"));
}
},
@@ -87,9 +87,9 @@ function DocumentMembersList({ document, invitedInSession }: Props) {
() =>
orderBy(
document.members,
(memberUser) =>
(invitedInSession.includes(memberUser.id) ? "_" : "") +
memberUser.name.toLocaleLowerCase(),
(user) =>
(invitedInSession.includes(user.id) ? "_" : "") +
user.name.toLocaleLowerCase(),
"asc"
),
[document.members, invitedInSession]
@@ -52,23 +52,10 @@ function PublicAccess({ document, share, sharedParent }: Props) {
}, [share?.urlId]);
const handleIndexingChanged = React.useCallback(
async (checked: boolean) => {
async (event) => {
try {
await share?.save({
allowIndexing: checked,
});
} catch (err) {
toast.error(err.message);
}
},
[share]
);
const handleShowLastModifiedChanged = React.useCallback(
async (checked: boolean) => {
try {
await share?.save({
showLastUpdated: checked,
allowIndexing: event.currentTarget.checked,
});
} catch (err) {
toast.error(err.message);
@@ -78,10 +65,10 @@ function PublicAccess({ document, share, sharedParent }: Props) {
);
const handlePublishedChange = React.useCallback(
async (checked: boolean) => {
async (event) => {
try {
await share?.save({
published: checked,
published: event.currentTarget.checked,
});
} catch (err) {
toast.error(err.message);
@@ -190,9 +177,7 @@ function PublicAccess({ document, share, sharedParent }: Props) {
"Disable this setting to discourage search engines from indexing the page"
)}
>
<NudeButton size={18}>
<QuestionMarkIcon size={18} />
</NudeButton>
<QuestionMarkIcon size={18} />
</Tooltip>
</Text>
}
@@ -208,34 +193,6 @@ function PublicAccess({ document, share, sharedParent }: Props) {
/>
)}
{share?.published && (
<ListItem
title={
<Text type="tertiary" as={Flex}>
{t("Show last modified")}&nbsp;
<Tooltip
content={t(
"Display the last modified timestamp on the shared page"
)}
>
<NudeButton size={18}>
<QuestionMarkIcon size={18} />
</NudeButton>
</Tooltip>
</Text>
}
actions={
<Switch
aria-label={t("Show last modified")}
checked={share?.showLastUpdated ?? false}
onChange={handleShowLastModifiedChanged}
width={26}
height={14}
/>
}
/>
)}
{sharedParent?.published ? (
<ShareLinkInput type="text" disabled defaultValue={shareUrl}>
{copyButton}
@@ -67,9 +67,9 @@ export const Suggestions = observer(
});
const fetchUsersByQuery = useThrottledCallback(
(searchQuery: string) => {
void users.fetchPage({ query: searchQuery });
void groups.fetchPage({ query: searchQuery });
(query: string) => {
void users.fetchPage({ query });
void groups.fetchPage({ query });
},
250,
undefined,
+12 -9
View File
@@ -61,13 +61,13 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
// suppresses text selection
event.preventDefault();
// this is simple because the sidebar is always against the left edge
const newWidth = Math.min(event.pageX - offset, maxWidth);
const isSmallerThanCollapsePoint = newWidth < minWidth / 2;
const width = Math.min(event.pageX - offset, maxWidth);
const isSmallerThanCollapsePoint = width < minWidth / 2;
ui.set({
sidebarWidth: isSmallerThanCollapsePoint
? theme.sidebarCollapsedWidth
: newWidth,
: width,
});
},
[ui, theme, offset, minWidth, maxWidth]
@@ -246,12 +246,7 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
/>
}
>
<NotificationsPopover>
<SidebarButton
position="bottom"
image={<NotificationIcon />}
/>
</NotificationsPopover>
<Notifications />
</SidebarButton>
)}
</AccountMenu>
@@ -266,6 +261,14 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
);
});
const Notifications = () => (
<NotificationsPopover>
{(rest: SidebarButtonProps) => (
<SidebarButton {...rest} position="bottom" image={<NotificationIcon />} />
)}
</NotificationsPopover>
);
const Backdrop = styled.a`
animation: ${fadeIn} 250ms ease-in-out;
position: fixed;
@@ -120,11 +120,7 @@ function InnerDocumentLink(
const handleDisclosureClick = React.useCallback(
(ev) => {
ev?.preventDefault();
if (expanded) {
setCollapsed();
} else {
setExpanded();
}
expanded ? setCollapsed() : setExpanded();
},
[setCollapsed, setExpanded, expanded]
);
@@ -75,7 +75,7 @@ const NavLink = ({
);
const { pathname: path } = toLocation;
const pathMatch = path
const match = path
? matchPath(currentLocation.pathname, {
// Regex taken from: https://github.com/pillarjs/path-to-regexp/blob/master/index.js#L202
path: path.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1"),
@@ -86,7 +86,7 @@ const NavLink = ({
const isActive =
preActive ??
!!(isActiveProp ? isActiveProp(pathMatch, currentLocation) : pathMatch);
!!(isActiveProp ? isActiveProp(match, currentLocation) : match);
const className = isActive
? joinClassnames(classNameProp, activeClassName)
: classNameProp;
@@ -86,12 +86,9 @@ function SharedWithMeLink({ membership, depth = 0 }: Props) {
);
const parentRef = React.useRef<HTMLDivElement>(null);
const reparentableNode = React.useMemo(
() => document?.asNavigationNode,
[document]
);
const node = React.useMemo(() => document?.asNavigationNode, [document]);
const [{ isOverReparent, canDropToReparent }, dropToReparent] =
useDropToReparentDocument(reparentableNode, setExpanded, parentRef);
useDropToReparentDocument(node, setExpanded, parentRef);
const { icon } = useSidebarLabelAndIcon(membership);
const [{ isDragging }, draggableRef] = useDragMembership(membership);
@@ -175,10 +172,10 @@ function SharedWithMeLink({ membership, depth = 0 }: Props) {
</Draggable>
</Relative>
<Folder expanded={displayChildDocuments}>
{childDocuments.map((childNode, index) => (
{childDocuments.map((node, index) => (
<DocumentLink
key={childNode.id}
node={childNode}
key={node.id}
node={node}
collection={collection}
activeDocument={documents.active}
isDraft={node.isDraft}
@@ -128,11 +128,11 @@ function StarredLink({ star }: Props) {
return null;
}
const documentCollection = document.collectionId
const collection = document.collectionId
? collections.get(document.collectionId)
: undefined;
const childDocuments = documentCollection
? documentCollection.getChildrenForDocument(documentId)
const childDocuments = collection
? collection.getChildrenForDocument(documentId)
: [];
const hasChildDocuments = childDocuments.length > 0;
@@ -176,7 +176,7 @@ function StarredLink({ star }: Props) {
<DocumentLink
key={node.id}
node={node}
collection={documentCollection}
collection={collection}
activeDocument={documents.active}
prefetchDocument={documents.prefetchDocument}
isDraft={node.isDraft}
+57 -62
View File
@@ -1,4 +1,3 @@
import * as RadixSwitch from "@radix-ui/react-switch";
import * as React from "react";
import styled from "styled-components";
import { s } from "@shared/styles";
@@ -6,11 +5,7 @@ import { LabelText } from "~/components/Input";
import Text from "~/components/Text";
import { undraggableOnDesktop } from "~/styles";
interface Props
extends Omit<
React.ComponentProps<typeof RadixSwitch.Root>,
"checked" | "onCheckedChange" | "onChange"
> {
interface Props extends React.HTMLAttributes<HTMLInputElement> {
/** Width of the switch. Defaults to 32. */
width?: number;
/** Height of the switch. Defaults to 18 */
@@ -27,8 +22,6 @@ interface Props
checked?: boolean;
/** Whether the switch is disabled */
disabled?: boolean;
/** Callback when the switch state changes */
onChange?: (checked: boolean) => void;
}
function Switch(
@@ -40,34 +33,26 @@ function Switch(
disabled,
className,
note,
checked,
onChange,
...props
}: Props,
ref: React.Ref<React.ElementRef<typeof RadixSwitch.Root>>
ref: React.Ref<HTMLInputElement>
) {
const handleCheckedChange = React.useCallback(
(checkedState: boolean) => {
if (onChange) {
onChange(checkedState);
}
},
[onChange]
);
const component = (
<StyledSwitchRoot
ref={ref}
checked={checked}
onCheckedChange={handleCheckedChange}
disabled={disabled}
<Input
width={width}
height={height}
className={label ? undefined : className}
{...props}
>
<StyledSwitchThumb width={width} height={height} />
</StyledSwitchRoot>
<HiddenInput
ref={ref}
type="checkbox"
width={width}
height={height}
disabled={disabled}
{...props}
/>
<Slider width={width} height={height} />
</Input>
);
if (label) {
@@ -125,50 +110,60 @@ const Label = styled.label<{
${(props) => (props.disabled ? `opacity: 0.75;` : "")}
`;
const StyledSwitchRoot = styled(RadixSwitch.Root)<{
width: number;
height: number;
}>`
const Input = styled.label<{ width: number; height: number }>`
position: relative;
display: inline-block;
width: ${(props) => props.width}px;
height: ${(props) => props.height}px;
background-color: ${(props) => props.theme.slate};
border-radius: ${(props) => props.height}px;
border: none;
cursor: var(--pointer);
transition: background-color 0.4s;
padding: 0 4px;
flex-shrink: 0;
`;
&:focus {
box-shadow: 0 0 1px ${s("accent")};
outline: none;
}
const Slider = styled.span<{ width: number; height: number }>`
position: absolute;
cursor: var(--pointer);
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: ${(props) => props.theme.slate};
-webkit-transition: 0.4s;
transition: 0.4s;
border-radius: ${(props) => props.height}px;
&[data-state="checked"] {
background-color: ${s("accent")};
}
&:disabled {
opacity: 0.75;
cursor: default;
&:before {
position: absolute;
content: "";
height: ${(props) => props.height - 8}px;
width: ${(props) => props.height - 8}px;
left: 4px;
bottom: 4px;
background-color: white;
border-radius: 50%;
-webkit-transition: 0.4s;
transition: 0.4s;
}
`;
const StyledSwitchThumb = styled(RadixSwitch.Thumb)<{
width: number;
height: number;
}>`
display: block;
width: ${(props) => props.height - 8}px;
height: ${(props) => props.height - 8}px;
background-color: white;
border-radius: 50%;
transition: transform 0.4s;
transform: translateX(0);
will-change: transform;
const HiddenInput = styled.input<{ width: number; height: number }>`
opacity: 0;
width: 0;
height: 0;
visibility: hidden;
&[data-state="checked"] {
&:disabled + ${Slider} {
opacity: 0.75;
cursor: default;
}
&:checked + ${Slider} {
background-color: ${s("accent")};
}
&:focus + ${Slider} {
box-shadow: 0 0 1px ${s("accent")};
}
&:checked + ${Slider}:before {
transform: translateX(${(props) => props.width - props.height}px);
}
`;
+8 -1
View File
@@ -27,6 +27,13 @@ function DocumentTemplatizeDialog({ documentId }: Props) {
document.collectionId ?? null
);
const handlePublishChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setPublish(ev.target.checked);
},
[]
);
const handleSubmit = React.useCallback(async () => {
const template = await document?.templatize({
collectionId,
@@ -65,7 +72,7 @@ function DocumentTemplatizeDialog({ documentId }: Props) {
label={t("Published")}
note={t("Enable other members to use the template immediately")}
checked={publish}
onChange={setPublish}
onChange={handlePublishChange}
/>
</Flex>
</ConfirmationDialog>
+153 -217
View File
@@ -1,138 +1,42 @@
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import Tippy, { TippyProps } from "@tippyjs/react";
import { transparentize } from "polished";
import * as React from "react";
import styled, { createGlobalStyle, keyframes } from "styled-components";
import styled, { createGlobalStyle } from "styled-components";
import { roundArrow } from "tippy.js";
import { s } from "@shared/styles";
import useMobile from "~/hooks/useMobile";
import { useTooltipContext } from "./TooltipContext";
export type Props = {
export type Props = Omit<TippyProps, "content" | "theme"> & {
/** The content to display in the tooltip. */
content?: React.ReactChild | React.ReactChild[];
/** A keyboard shortcut to display next to the content */
shortcut?: React.ReactNode;
/** Whether to show the shortcut on a new line */
shortcutOnNewline?: boolean;
/** The preferred side of the trigger to render against when open */
side?: "top" | "right" | "bottom" | "left";
/** The distance in pixels from the trigger */
sideOffset?: number;
/** The preferred alignment against the trigger */
align?: "start" | "center" | "end";
/** An offset in pixels from the "start" or "end" alignment options */
alignOffset?: number;
/** When true, overrides the side and align preferences to prevent collisions with boundary edges */
avoidCollisions?: boolean;
/** The element used as the collision boundary */
collisionBoundary?: Element | null | Array<Element | null>;
/** The distance in pixels from the boundary edges where collision detection should occur */
collisionPadding?:
| number
| Partial<Record<"top" | "right" | "bottom" | "left", number>>;
/** Whether the tooltip should be open by default */
defaultOpen?: boolean;
/** The controlled open state of the tooltip */
open?: boolean;
/** Event handler called when the open state of the tooltip changes */
onOpenChange?: (open: boolean) => void;
/** The duration from when the mouse enters the trigger until the tooltip gets opened */
delayDuration?: number;
/** How much time a user has to enter another trigger without incurring a delay again */
skipDelayDuration?: number;
/** Prevents the tooltip from opening */
disableHoverableContent?: boolean;
/** The children that will trigger the tooltip */
children?: React.ReactNode;
/** Whether to disable the tooltip entirely */
disabled?: boolean;
/** Custom offset for the tooltip */
offset?: [number, number];
/** Placement prop for backward compatibility with Tippy */
placement?:
| "top"
| "right"
| "bottom"
| "left"
| "top-start"
| "top-end"
| "right-start"
| "right-end"
| "bottom-start"
| "bottom-end"
| "left-start"
| "left-end";
/** Delay prop for backward compatibility with Tippy */
delay?: number | [number, number];
};
/**
* Tooltip component using Radix UI primitives.
* Displays a tooltip with optional keyboard shortcut.
* Optionally displays a keyboard shortcut next to the content.
* A tooltip component that wraps Tippy and provides a consistent look and feel. Optionally
* displays a keyboard shortcut next to the content.
*
* Wrap this component in a TooltipProvider to allow multiple tooltips to share the same
* provider instance (delay, animation, etc).
* singleton instance (delay, animation, etc).
*/
function Tooltip({
shortcut,
shortcutOnNewline,
content: tooltip,
side = "top",
sideOffset = 8,
align = "center",
alignOffset = 0,
avoidCollisions = true,
collisionBoundary,
collisionPadding = 8,
defaultOpen,
open,
onOpenChange,
delayDuration = 500,
skipDelayDuration = 300,
disableHoverableContent = false,
children,
disabled = false,
offset,
placement,
delay,
delay = 500,
...rest
}: Props): React.ReactElement | null {
}: Props) {
const isMobile = useMobile();
const isInProvider = useTooltipContext();
// Handle backward compatibility with Tippy props
let finalSide = side;
let finalAlign = align;
let finalDelayDuration = delayDuration;
let finalSideOffset = sideOffset;
// Convert placement prop to side/align for backward compatibility
if (placement) {
const [placementSide, placementAlign] = placement.split("-");
finalSide = placementSide as "top" | "right" | "bottom" | "left";
if (placementAlign) {
finalAlign = placementAlign as "start" | "center" | "end";
}
}
// Handle delay prop for backward compatibility
if (delay !== undefined) {
if (typeof delay === "number") {
finalDelayDuration = delay;
} else if (Array.isArray(delay)) {
finalDelayDuration = delay[0];
}
}
// Handle offset prop for backward compatibility
if (offset) {
finalSideOffset = offset[1] || sideOffset;
}
const singleton = useTooltipContext();
let content = <>{tooltip}</>;
if (!tooltip || isMobile || disabled) {
return (children as React.ReactElement) ?? null;
if (!tooltip || isMobile) {
return rest.children ?? null;
}
if (shortcut) {
@@ -155,92 +59,20 @@ function Tooltip({
);
}
const tooltipContent = (
<TooltipPrimitive.Root
defaultOpen={defaultOpen}
open={open}
onOpenChange={onOpenChange}
delayDuration={isInProvider ? undefined : finalDelayDuration}
disableHoverableContent={disableHoverableContent}
>
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
<TooltipPrimitive.Portal>
<StyledContent
side={finalSide}
sideOffset={finalSideOffset}
align={finalAlign}
alignOffset={alignOffset}
avoidCollisions={avoidCollisions}
collisionBoundary={collisionBoundary}
collisionPadding={collisionPadding}
{...rest}
>
{content}
</StyledContent>
</TooltipPrimitive.Portal>
</TooltipPrimitive.Root>
);
// If we're already in a provider, don't wrap with another one
if (isInProvider) {
return tooltipContent;
}
// Otherwise, wrap with a provider for standalone usage
return (
<TooltipPrimitive.Provider
delayDuration={finalDelayDuration}
skipDelayDuration={skipDelayDuration}
>
{tooltipContent}
</TooltipPrimitive.Provider>
<Tippy
arrow={roundArrow}
content={content}
delay={delay}
animation="shift-away"
singleton={singleton}
duration={[200, 150]}
inertia
{...rest}
/>
);
}
const slideUpAndFade = keyframes`
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
`;
const slideRightAndFade = keyframes`
from {
opacity: 0;
transform: translateX(-8px);
}
to {
opacity: 1;
transform: translateX(0);
}
`;
const slideDownAndFade = keyframes`
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
`;
const slideLeftAndFade = keyframes`
from {
opacity: 0;
transform: translateX(8px);
}
to {
opacity: 1;
transform: translateX(0);
}
`;
const Shortcut = styled.kbd`
position: relative;
top: -1px;
@@ -257,36 +89,140 @@ const Shortcut = styled.kbd`
border-radius: 3px;
`;
const StyledContent = styled(TooltipPrimitive.Content)`
position: relative;
background-color: ${s("tooltipBackground")};
color: ${s("tooltipText")};
border-radius: 4px;
font-size: 13px;
line-height: 1.4;
white-space: normal;
outline: 0;
padding: 5px 9px;
z-index: 9999;
max-width: calc(100vw - 10px);
export const TooltipStyles = createGlobalStyle`
.tippy-box[data-animation=fade][data-state=hidden]{
opacity:0
}
[data-tippy-root]{
max-width:calc(100vw - 10px)
}
.tippy-box{
position:relative;
background-color: ${s("tooltipBackground")};
color: ${s("tooltipText")};
border-radius:4px;
font-size:13px;
line-height:1.4;
white-space:normal;
outline:0;
transition-property:transform,visibility,opacity
}
.tippy-box[data-placement^=top]>.tippy-arrow{
bottom:0
}
.tippy-box[data-placement^=top]>.tippy-arrow:before{
bottom:-7px;
left:0;
border-width:8px 8px 0;
border-top-color:initial;
transform-origin:center top
}
.tippy-box[data-placement^=bottom]>.tippy-arrow{
top:0
}
.tippy-box[data-placement^=bottom]>.tippy-arrow:before{
top:-7px;
left:0;
border-width:0 8px 8px;
border-bottom-color:initial;
transform-origin:center bottom
}
.tippy-box[data-placement^=left]>.tippy-arrow{
right:0
}
.tippy-box[data-placement^=left]>.tippy-arrow:before{
border-width:8px 0 8px 8px;
border-left-color:initial;
right:-7px;
transform-origin:center left
}
.tippy-box[data-placement^=right]>.tippy-arrow{
left:0
}
.tippy-box[data-placement^=right]>.tippy-arrow:before{
left:-7px;
border-width:8px 8px 8px 0;
border-right-color:initial;
transform-origin:center right
}
.tippy-box[data-inertia][data-state=visible]{
transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)
}
.tippy-arrow{
width:16px;
height:16px;
color: ${s("tooltipBackground")};
}
.tippy-arrow:before{
content:"";
position:absolute;
border-color:transparent;
border-style:solid
}
.tippy-content{
position:relative;
padding:5px 9px;
z-index:1
}
/* Arrow Styles */
.tippy-box[data-placement^=top]>.tippy-svg-arrow{
bottom:0
}
.tippy-box[data-placement^=top]>.tippy-svg-arrow:after,.tippy-box[data-placement^=top]>.tippy-svg-arrow>svg{
top:16px;
transform:rotate(180deg)
}
.tippy-box[data-placement^=bottom]>.tippy-svg-arrow{
top:0
}
.tippy-box[data-placement^=bottom]>.tippy-svg-arrow>svg{
bottom:16px
}
.tippy-box[data-placement^=left]>.tippy-svg-arrow{
right:0
}
.tippy-box[data-placement^=left]>.tippy-svg-arrow:after,.tippy-box[data-placement^=left]>.tippy-svg-arrow>svg{
transform:rotate(90deg);
top:calc(50% - 3px);
left:11px
}
.tippy-box[data-placement^=right]>.tippy-svg-arrow{
left:0
}
.tippy-box[data-placement^=right]>.tippy-svg-arrow:after,.tippy-box[data-placement^=right]>.tippy-svg-arrow>svg{
transform:rotate(-90deg);
top:calc(50% - 3px);
right:11px
}
.tippy-svg-arrow{
width:16px;
height:16px;
fill: ${s("tooltipBackground")};
text-align:initial
}
.tippy-svg-arrow,.tippy-svg-arrow>svg{
position:absolute
}
/* Animation */
&[data-state="delayed-open"][data-side="top"] {
animation: ${slideUpAndFade} 200ms cubic-bezier(0.16, 1, 0.3, 1);
}
&[data-state="delayed-open"][data-side="right"] {
animation: ${slideLeftAndFade} 200ms cubic-bezier(0.16, 1, 0.3, 1);
}
&[data-state="delayed-open"][data-side="bottom"] {
animation: ${slideDownAndFade} 200ms cubic-bezier(0.16, 1, 0.3, 1);
}
&[data-state="delayed-open"][data-side="left"] {
animation: ${slideRightAndFade} 200ms cubic-bezier(0.16, 1, 0.3, 1);
}
`;
.tippy-box[data-animation=shift-away][data-state=hidden]{opacity:0}.tippy-box[data-animation=shift-away][data-state=hidden][data-placement^=top]{transform:translateY(10px)}.tippy-box[data-animation=shift-away][data-state=hidden][data-placement^=bottom]{transform:translateY(-10px)}.tippy-box[data-animation=shift-away][data-state=hidden][data-placement^=left]{transform:translateX(10px)}.tippy-box[data-animation=shift-away][data-state=hidden][data-placement^=right]{transform:translateX(-10px)}
export const TooltipStyles = createGlobalStyle`
/* Legacy styles for backward compatibility - can be removed after migration */
.tippy-box[data-animation=shift-away][data-state=hidden]{
opacity:0
}
.tippy-box[data-animation=shift-away][data-state=hidden][data-placement^=top]{
transform:translateY(10px)
}
.tippy-box[data-animation=shift-away][data-state=hidden][data-placement^=bottom]{
transform:translateY(-10px)
}
.tippy-box[data-animation=shift-away][data-state=hidden][data-placement^=left]{
transform:translateX(10px)
}
.tippy-box[data-animation=shift-away][data-state=hidden][data-placement^=right]{
transform:translateX(-10px)
}
`;
export default Tooltip;
+23 -30
View File
@@ -1,7 +1,9 @@
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import Tippy, { useSingleton, TippyProps } from "@tippyjs/react";
import * as React from "react";
import { roundArrow } from "tippy.js";
export const TooltipContext = React.createContext<boolean>(false);
export const TooltipContext =
React.createContext<TippyProps["singleton"]>(undefined);
export function useTooltipContext() {
return React.useContext(TooltipContext);
@@ -9,39 +11,30 @@ export function useTooltipContext() {
type Props = {
children: React.ReactNode;
/** The duration from when the mouse enters the trigger until the tooltip gets opened */
delayDuration?: number;
/** How much time a user has to enter another trigger without incurring a delay again */
skipDelayDuration?: number;
/** Prevents the tooltip from opening */
disableHoverableContent?: boolean;
/** Props to pass to the Tippy component - kept for backward compatibility */
tippyProps?: {
delay?: number;
[key: string]: unknown;
};
/** Props to pass to the Tippy component */
tippyProps?: TippyProps;
};
/**
* Wrap a collection of tooltips in a provider to allow them to share the same provider instance.
* Wrap a collection of tooltips in a provider to allow them to share the same singleton instance.
*/
export function TooltipProvider({
children,
delayDuration = 500,
skipDelayDuration = 300,
disableHoverableContent = false,
tippyProps,
}: Props) {
// Handle backward compatibility with tippyProps
const finalDelayDuration = tippyProps?.delay ?? delayDuration;
export function TooltipProvider({ children, tippyProps }: Props) {
const [source, target] = useSingleton();
return (
<TooltipPrimitive.Provider
delayDuration={finalDelayDuration}
skipDelayDuration={skipDelayDuration}
disableHoverableContent={disableHoverableContent}
>
<TooltipContext.Provider value={true}>{children}</TooltipContext.Provider>
</TooltipPrimitive.Provider>
<>
<Tippy
delay={500}
arrow={roundArrow}
animation="shift-away"
singleton={source}
duration={[200, 150]}
inertia
{...tippyProps}
/>
<TooltipContext.Provider value={target}>
{children}
</TooltipContext.Provider>
</>
);
}
+2 -14
View File
@@ -159,16 +159,10 @@ class WebsocketProvider extends Component<Props> {
if (document?.updatedAt === documentDescriptor.updatedAt) {
continue;
}
if (!document) {
if (!document && !event.fetchIfMissing) {
continue;
}
if (event.invalidatedPolicies) {
event.invalidatedPolicies.forEach((policyId) => {
policies.remove(policyId);
});
}
// otherwise, grab the latest version of the document
try {
document = await documents.fetch(documentId, {
@@ -213,16 +207,10 @@ class WebsocketProvider extends Component<Props> {
if (collection?.updatedAt === collectionDescriptor.updatedAt) {
continue;
}
if (!collection?.documents) {
if (!collection?.documents && !event.fetchIfMissing) {
continue;
}
if (event.invalidatedPolicies) {
event.invalidatedPolicies.forEach((policyId) => {
policies.remove(policyId);
});
}
try {
await collection?.fetchDocuments({
force: true,
+1 -1
View File
@@ -190,7 +190,7 @@ const LinkEditor: React.FC<Props> = ({
try {
onClickLink(getHref(), event);
} catch (_err) {
} catch (err) {
toast.error(dictionary.openLinkError);
}
};
-252
View File
@@ -1,252 +0,0 @@
import { NodeSelection } from "prosemirror-state";
import { useCallback, useEffect, useRef, useState } from "react";
import styled from "styled-components";
import Flex from "@shared/components/Flex";
import Text from "@shared/components/Text";
import { EditorStyleHelper } from "@shared/editor/styles/EditorStyleHelper";
import { extraArea } from "@shared/styles";
import Input, { NativeInput, Outline } from "~/components/Input";
import { useEditor } from "./EditorContext";
type Dimension = {
width: string;
height: string;
changed: "width" | "height" | "none";
};
export function MediaDimension() {
const ref = useRef<HTMLDivElement>(null);
const boundsRef = useRef<{
width: { min: number; max: number };
height: { min: number; max: number };
}>();
const { view, commands } = useEditor();
const { state } = view;
const { selection } = state;
// This component will be rendered only when the selection is image or video (NodeSelection types).
const node = (selection as NodeSelection).node;
const nodeType = node.type.name,
width = node.attrs.width as number,
height = node.attrs.height as number;
const [localDimension, setLocalDimension] = useState<Dimension>(() => ({
width: String(width),
height: String(height),
changed: "none",
}));
const [error, setError] = useState<{ width: boolean; height: boolean }>({
width: false,
height: false,
});
if (!boundsRef.current && ref.current) {
const docWidth = parseInt(
getComputedStyle(ref.current).getPropertyValue("--document-width")
);
const maxWidth = docWidth - EditorStyleHelper.padding * 2;
const constrainedWidth = Math.min(width, maxWidth); // Ensure media width does not exceed the max width of the editor.
const aspectRatio = height / constrainedWidth;
const maxHeight = Math.round(maxWidth * aspectRatio);
boundsRef.current = {
width: { min: 50, max: maxWidth },
height: { min: 50, max: maxHeight },
};
}
const reset = useCallback(() => {
setLocalDimension({
width: String(width),
height: String(height),
changed: "none",
});
setError({ width: false, height: false });
}, [width, height]);
const isOutsideBounds = useCallback(
(type: "width" | "height", value: number) => {
const bounds = boundsRef.current!;
return value < bounds[type].min || value > bounds[type].max;
},
[]
);
const handleChange = useCallback(
(type: "width" | "height") => (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
const isNumber = /^\d+$/.test(value);
if (value && (!isNumber || value === "0")) {
return;
}
setError((prev) => {
if (!prev.width && !prev.height) {
return prev;
}
return { width: false, height: false };
});
setLocalDimension((prev) => {
if (type === "width") {
return {
...prev,
width: value,
changed: "width",
};
}
return {
...prev,
height: value,
changed: "height",
};
});
},
[]
);
const handleBlur = useCallback(() => {
const localWidthAsNumber = localDimension.width
? parseInt(localDimension.width, 10)
: undefined,
localHeightAsNumber = localDimension.height
? parseInt(localDimension.height, 10)
: undefined;
const isUnchanged =
!localWidthAsNumber ||
!localHeightAsNumber ||
(localWidthAsNumber === width && localHeightAsNumber === height);
const isError =
error.width ||
error.height ||
(localDimension.changed === "width" &&
localWidthAsNumber &&
isOutsideBounds("width", localWidthAsNumber)); // check width bounds here since 'onChange' error checker is debounced.
if (isUnchanged || isError) {
reset();
return;
}
const maxWidth = boundsRef.current!.width.max;
// For images resized to the full width of the editor, natural width will be shown in the toolbar.
// So, we constrain it here for computing aspect ratio.
const constrainedWidth = Math.min(width, maxWidth);
const aspectRatio =
localDimension.changed === "width"
? height / constrainedWidth
: constrainedWidth / height;
const finalWidth =
localDimension.changed === "width"
? localWidthAsNumber
: Math.round(aspectRatio * localHeightAsNumber);
const finalHeight =
localDimension.changed === "height"
? localHeightAsNumber
: Math.round(aspectRatio * localWidthAsNumber);
if (nodeType === "image") {
commands["resizeImage"]({
width: finalWidth,
height: finalHeight,
});
}
}, [commands, width, height, localDimension, nodeType, error, reset]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
handleBlur();
} else if (e.key === "Escape") {
reset();
}
},
[handleBlur, reset]
);
// Sync dimension changes from outside.
useEffect(() => {
if (
width !== Number(localDimension.width) ||
height !== Number(localDimension.height)
) {
reset();
}
}, [width, height, reset]);
// hacky debounce for checking error.
useEffect(() => {
const timeout = setTimeout(() => {
const isWidthError = localDimension.width
? Number(localDimension.width) !== width &&
isOutsideBounds("width", Number(localDimension.width))
: false;
const isHeightError = localDimension.height
? Number(localDimension.height) !== height &&
isOutsideBounds("height", Number(localDimension.height))
: false;
if (isWidthError || isHeightError) {
setError({
width: isWidthError,
height: isHeightError,
});
}
}, 200);
return () => clearTimeout(timeout);
}, [width, height, localDimension, isOutsideBounds]);
return (
<StyledFlex ref={ref} align="center">
<StyledInput
value={localDimension.width}
onChange={handleChange("width")}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
$error={error.width}
/>
<Text size="xsmall" type="tertiary">
x
</Text>
<StyledInput
value={localDimension.height}
onChange={handleChange("height")}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
$error={error.height}
/>
</StyledFlex>
);
}
const StyledFlex = styled(Flex)`
pointer-events: all;
position: relative;
${extraArea(4)}
`;
const StyledInput = styled(Input)<{ $error?: boolean }>`
width: 50px;
z-index: 1;
${Outline} {
margin: 0;
background: transparent;
border-color: transparent;
}
${NativeInput} {
height: 24px;
padding: 0;
text-align: center;
${(props) => props.$error && `color: ${props.theme.danger}`};
}
`;
@@ -1,6 +1,5 @@
import some from "lodash/some";
import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
import { CellSelection } from "prosemirror-tables";
import * as React from "react";
import filterExcessSeparators from "@shared/editor/lib/filterExcessSeparators";
import { getMarkRange } from "@shared/editor/queries/getMarkRange";
@@ -23,7 +22,6 @@ import getImageMenuItems from "../menus/image";
import getNoticeMenuItems from "../menus/notice";
import getReadOnlyMenuItems from "../menus/readOnly";
import getTableMenuItems from "../menus/table";
import getTableCellMenuItems from "../menus/tableCell";
import getTableColMenuItems from "../menus/tableCol";
import getTableRowMenuItems from "../menus/tableRow";
import { useEditor } from "./EditorContext";
@@ -185,7 +183,6 @@ export default function SelectionToolbar(props: Props) {
const colIndex = getColumnIndex(state);
const rowIndex = getRowIndex(state);
const isTableSelection = colIndex !== undefined && rowIndex !== undefined;
const isCellSelection = selection instanceof CellSelection;
const link = getMarkRange(selection.$from, state.schema.marks.link);
const isImageSelection =
selection instanceof NodeSelection && selection.node.type.name === "image";
@@ -205,8 +202,6 @@ export default function SelectionToolbar(props: Props) {
items = getTableColMenuItems(state, colIndex, rtl, dictionary);
} else if (rowIndex !== undefined) {
items = getTableRowMenuItems(state, rowIndex, dictionary);
} else if (isCellSelection) {
items = getTableCellMenuItems(state, dictionary);
} else if (isImageSelection) {
items = readOnly ? [] : getImageMenuItems(state, dictionary);
} else if (isAttachmentSelection) {
@@ -226,9 +221,6 @@ export default function SelectionToolbar(props: Props) {
if (item.name === "separator") {
return true;
}
if (item.name === "dimensions") {
return item.visible ?? false;
}
if (item.name && !commands[item.name]) {
return false;
}
+3 -3
View File
@@ -1,9 +1,9 @@
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
import commandScore from "command-score";
import capitalize from "lodash/capitalize";
import orderBy from "lodash/orderBy";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import { VisuallyHidden } from "reakit/VisuallyHidden";
import { toast } from "sonner";
import styled from "styled-components";
import insertFiles from "@shared/editor/commands/insertFiles";
@@ -672,7 +672,7 @@ function SuggestionsMenu<T extends MenuItem>(props: Props<T>) {
</List>
)}
{uploadFile && (
<VisuallyHidden.Root>
<VisuallyHidden>
<label>
<Trans>Import document</Trans>
<input
@@ -682,7 +682,7 @@ function SuggestionsMenu<T extends MenuItem>(props: Props<T>) {
multiple
/>
</label>
</VisuallyHidden.Root>
</VisuallyHidden>
)}
</>
)}
+6 -6
View File
@@ -1,3 +1,4 @@
import { TippyProps } from "@tippyjs/react";
import { useMemo } from "react";
import { useMenuState } from "reakit";
import { MenuButton } from "reakit/Menu";
@@ -10,7 +11,6 @@ import Template from "~/components/ContextMenu/Template";
import { TooltipProvider } from "~/components/TooltipContext";
import { MenuItem as TMenuItem } from "~/types";
import { useEditor } from "./EditorContext";
import { MediaDimension } from "./MediaDimension";
import ToolbarButton from "./ToolbarButton";
import ToolbarSeparator from "./ToolbarSeparator";
import Tooltip from "./Tooltip";
@@ -77,6 +77,8 @@ function ToolbarDropdown(props: { active: boolean; item: MenuItem }) {
);
}
const tippyProps = { placement: "top" } as TippyProps;
function ToolbarMenu(props: Props) {
const { commands, view } = useEditor();
const { items } = props;
@@ -93,13 +95,13 @@ function ToolbarMenu(props: Props) {
};
return (
<TooltipProvider>
<TooltipProvider tippyProps={tippyProps}>
<FlexibleWrapper>
{items.map((item, index) => {
if (item.name === "separator" && item.visible !== false) {
return <ToolbarSeparator key={index} />;
}
if (item.visible === false || (!item.skipIcon && !item.icon)) {
if (item.visible === false || !item.icon) {
return null;
}
const isActive = item.active ? item.active(state) : false;
@@ -110,9 +112,7 @@ function ToolbarMenu(props: Props) {
shortcut={item.shortcut}
content={item.label === item.tooltip ? undefined : item.tooltip}
>
{item.name === "dimensions" ? (
<MediaDimension key={index} />
) : item.children ? (
{item.children ? (
<ToolbarDropdown active={isActive && !item.label} item={item} />
) : (
<ToolbarButton
+3 -3
View File
@@ -8,10 +8,10 @@ const WrappedTooltip: React.FC<Props> = ({
...rest
}: Props) => (
<Tooltip
sideOffset={16}
delayDuration={150}
offset={[0, 16]}
delay={150}
content={content}
side="top"
placement="top"
shortcutOnNewline
{...rest}
>
+1 -1
View File
@@ -299,7 +299,7 @@ export default class FindAndReplaceExtension extends Extension {
this.results.push({ from, to, type });
}
} catch (_err) {
} catch (e) {
// Invalid RegExp
}
});
+4 -6
View File
@@ -293,11 +293,9 @@ export default class PasteHandler extends Extension {
currentPos += node.nodeSize;
});
} else {
if (singleNode) {
tr.replaceSelectionWith(singleNode, this.shiftKey);
} else {
tr.replaceSelection(slice);
}
singleNode
? tr.replaceSelectionWith(singleNode, this.shiftKey)
: tr.replaceSelection(slice);
}
view.dispatch(
@@ -552,7 +550,7 @@ function parseSingleIframeSrc(html: string) {
return src;
}
}
} catch (_err) {
} catch (e) {
// Ignore the million ways parsing could fail.
}
return undefined;
+1 -1
View File
@@ -513,7 +513,7 @@ export class Editor extends React.PureComponent<
},
this.elementRef.current || undefined
);
} catch (_err) {
} catch (err) {
// querySelector will throw an error if the hash begins with a number
// or contains a period. This is protected against now by safeSlugify
// however previous links may be in the wild.
-9
View File
@@ -59,15 +59,6 @@ export default function imageMenuItems(
{
name: "separator",
},
{
name: "dimensions",
tooltip: dictionary.dimensions,
visible: !isFullWidthAligned(state),
skipIcon: true,
},
{
name: "separator",
},
{
name: "downloadImage",
tooltip: dictionary.downloadImage,
-36
View File
@@ -1,36 +0,0 @@
import { TableSplitCellsIcon, TableMergeCellsIcon } from "outline-icons";
import { EditorState } from "prosemirror-state";
import { CellSelection } from "prosemirror-tables";
import {
isMergedCellSelection,
isMultipleCellSelection,
} from "@shared/editor/queries/table";
import { MenuItem } from "@shared/editor/types";
import { Dictionary } from "~/hooks/useDictionary";
export default function tableCellMenuItems(
state: EditorState,
dictionary: Dictionary
): MenuItem[] {
const { selection } = state;
// Only show menu items if we have a CellSelection
if (!(selection instanceof CellSelection)) {
return [];
}
return [
{
name: "mergeCells",
label: dictionary.mergeCells,
icon: <TableMergeCellsIcon />,
visible: isMultipleCellSelection(state),
},
{
name: "splitCell",
label: dictionary.splitCell,
icon: <TableSplitCellsIcon />,
visible: isMergedCellSelection(state),
},
];
}
+1 -27
View File
@@ -8,17 +8,10 @@ import {
ArrowIcon,
MoreIcon,
TableHeaderColumnIcon,
TableMergeCellsIcon,
TableSplitCellsIcon,
} from "outline-icons";
import { EditorState } from "prosemirror-state";
import { CellSelection } from "prosemirror-tables";
import styled from "styled-components";
import { isNodeActive } from "@shared/editor/queries/isNodeActive";
import {
isMergedCellSelection,
isMultipleCellSelection,
} from "@shared/editor/queries/table";
import { MenuItem } from "@shared/editor/types";
import { Dictionary } from "~/hooks/useDictionary";
@@ -28,11 +21,7 @@ export default function tableColMenuItems(
rtl: boolean,
dictionary: Dictionary
): MenuItem[] {
const { schema, selection } = state;
if (!(selection instanceof CellSelection)) {
return [];
}
const { schema } = state;
return [
{
@@ -107,21 +96,6 @@ export default function tableColMenuItems(
icon: <InsertRightIcon />,
attrs: { index },
},
{
name: "mergeCells",
label: dictionary.mergeCells,
icon: <TableMergeCellsIcon />,
visible: isMultipleCellSelection(state),
},
{
name: "splitCell",
label: dictionary.splitCell,
icon: <TableSplitCellsIcon />,
visible: isMergedCellSelection(state),
},
{
name: "separator",
},
{
name: "deleteColumn",
dangerous: true,
-27
View File
@@ -4,15 +4,8 @@ import {
InsertBelowIcon,
MoreIcon,
TableHeaderRowIcon,
TableSplitCellsIcon,
TableMergeCellsIcon,
} from "outline-icons";
import { EditorState } from "prosemirror-state";
import { CellSelection } from "prosemirror-tables";
import {
isMergedCellSelection,
isMultipleCellSelection,
} from "@shared/editor/queries/table";
import { MenuItem } from "@shared/editor/types";
import { Dictionary } from "~/hooks/useDictionary";
@@ -21,11 +14,6 @@ export default function tableRowMenuItems(
index: number,
dictionary: Dictionary
): MenuItem[] {
const { selection } = state;
if (!(selection instanceof CellSelection)) {
return [];
}
return [
{
icon: <MoreIcon />,
@@ -48,21 +36,6 @@ export default function tableRowMenuItems(
icon: <InsertBelowIcon />,
attrs: { index },
},
{
name: "mergeCells",
label: dictionary.mergeCells,
icon: <TableMergeCellsIcon />,
visible: isMultipleCellSelection(state),
},
{
name: "splitCell",
label: dictionary.splitCell,
icon: <TableSplitCellsIcon />,
visible: isMergedCellSelection(state),
},
{
name: "separator",
},
{
name: "deleteRow",
label: dictionary.deleteRow,
-3
View File
@@ -35,7 +35,6 @@ export default function useDictionary() {
deleteRow: t("Delete"),
deleteTable: t("Delete table"),
deleteAttachment: t("Delete file"),
dimensions: t("Width x Height"),
download: t("Download"),
downloadAttachment: t("Download file"),
replaceAttachment: t("Replace file"),
@@ -87,8 +86,6 @@ export default function useDictionary() {
toggleHeader: t("Toggle header"),
mathInline: t("Math inline (LaTeX)"),
mathBlock: t("Math block (LaTeX)"),
mergeCells: t("Merge cells"),
splitCell: t("Split cell"),
tip: t("Tip"),
tipNotice: t("Tip notice"),
warning: t("Warning"),
+1 -1
View File
@@ -30,7 +30,7 @@ export default function useEditorClickHandlers({ shareId }: Params) {
try {
const url = new URL(href);
navigateTo = url.pathname + url.hash;
} catch (_err) {
} catch (err) {
navigateTo = href;
}
}
+4 -1
View File
@@ -36,7 +36,10 @@ export default function useEmbeds(loadIfMissing = false) {
embeds.map((e) => {
// Find any integrations that match this embed and inject the settings
const integration: Integration<IntegrationType.Embed> | undefined =
find(integrations.orderedData, (i) => i.service === e.name);
find(
integrations.orderedData,
(integration) => integration.service === e.name
);
if (integration?.settings) {
e.settings = integration.settings;
+3 -3
View File
@@ -43,7 +43,7 @@ export function setPostLoginPath(path: string) {
try {
sessionStorage.setItem(key, path);
} catch (_err) {
} catch (e) {
// If the session storage is full or inaccessible, we can't do anything about it.
}
}
@@ -62,7 +62,7 @@ export function usePostLoginPath() {
let path;
try {
path = sessionStorage.getItem(key) || getCookie(key);
} catch (_err) {
} catch (e) {
// Expected error if the session storage is full or inaccessible.
}
@@ -74,7 +74,7 @@ export function usePostLoginPath() {
const cleanup = history.listen(() => {
try {
sessionStorage.removeItem(key);
} catch (_err) {
} catch (e) {
// Expected error if the session storage is full or inaccessible.
}
removeCookie(key);
+7 -47
View File
@@ -2,56 +2,24 @@ import noop from "lodash/noop";
import * as React from "react";
type MenuContextType = {
isMenuOpen: boolean;
setIsMenuOpen: React.Dispatch<React.SetStateAction<boolean>>;
registerMenu: (menuId: string, hideFunction: () => void) => void;
unregisterMenu: (menuId: string) => void;
closeOtherMenus: (...menuIds: (string | undefined)[]) => void;
openMenuHideFn: (() => void) | null;
setOpenMenuHideFn: React.Dispatch<React.SetStateAction<(() => void) | null>>;
};
const MenuContext = React.createContext<MenuContextType | null>(null);
// Registry to track all active menu instances
const menuRegistry = new Map();
type Props = {
children?: React.ReactNode;
};
export const MenuProvider: React.FC = ({ children }: Props) => {
const [isMenuOpen, setIsMenuOpen] = React.useState(false);
const registerMenu = React.useCallback(
(menuId: string, hideFunction: () => void) => {
menuRegistry.set(menuId, hideFunction);
},
[]
);
const unregisterMenu = React.useCallback((menuId: string) => {
menuRegistry.delete(menuId);
}, []);
const closeOtherMenus = React.useCallback(
(...menuIds: (string | undefined)[]) => {
menuRegistry.forEach((hideFunction, menuId) => {
if (!menuIds.includes(menuId)) {
hideFunction();
}
});
},
[]
);
const [openMenuHideFn, setOpenMenuHideFn] = React.useState<(() => void) | null>(null);
const memoized = React.useMemo(
() => ({
isMenuOpen,
setIsMenuOpen,
registerMenu,
unregisterMenu,
closeOtherMenus,
openMenuHideFn,
setOpenMenuHideFn,
}),
[isMenuOpen, setIsMenuOpen, registerMenu, unregisterMenu, closeOtherMenus]
[openMenuHideFn, setOpenMenuHideFn]
);
return (
@@ -61,15 +29,7 @@ export const MenuProvider: React.FC = ({ children }: Props) => {
const useMenuContext: () => MenuContextType = () => {
const value = React.useContext(MenuContext);
return value
? value
: {
isMenuOpen: false,
setIsMenuOpen: noop,
registerMenu: noop,
unregisterMenu: noop,
closeOtherMenus: noop,
};
return value ? value : { openMenuHideFn: null, setOpenMenuHideFn: noop };
};
export default useMenuContext;
+2 -2
View File
@@ -23,11 +23,11 @@ const useMenuHeight = ({
React.useLayoutEffect(() => {
if (visible && !isMobile) {
const calculatedMaxHeight = (windowHeight / 100) * maxViewportHeight;
const maxHeight = (windowHeight / 100) * maxViewportHeight;
setMaxHeight(
Math.min(
calculatedMaxHeight,
maxHeight,
elementRef?.current
? windowHeight -
elementRef.current.getBoundingClientRect().bottom -
-48
View File
@@ -1,48 +0,0 @@
import * as React from "react";
import {
// eslint-disable-next-line no-restricted-imports
useMenuState as reakitUseMenuState,
MenuStateReturn,
} from "reakit/Menu";
import useMenuContext from "./useMenuContext";
type Props = Parameters<typeof reakitUseMenuState>[0] & {
parentId?: string;
};
/**
* A hook that wraps Reakit's useMenuState with coordination logic to ensure
* only one context menu can be open at a time across the application.
*/
export function useMenuState(options?: Props): MenuStateReturn {
const menuState = reakitUseMenuState(options);
const { registerMenu, unregisterMenu, closeOtherMenus } = useMenuContext();
const menuId = menuState.baseId;
const parentId = options?.parentId;
// Register this menu instance on mount and unregister on unmount
React.useEffect(() => {
registerMenu(menuId, menuState.hide);
return () => unregisterMenu(menuId);
}, [menuId, menuState.hide, registerMenu, unregisterMenu]);
const coordinatedShow = React.useCallback(() => {
closeOtherMenus(menuId, parentId);
menuState.show();
}, [closeOtherMenus, menuId, menuState, parentId]);
const coordinatedToggle = React.useCallback(() => {
closeOtherMenus(menuId, parentId);
menuState.toggle();
}, [menuId, menuState, closeOtherMenus, parentId]);
// Return the menu state with the coordinated show method
return React.useMemo(
() => ({
...menuState,
toggle: coordinatedToggle,
show: coordinatedShow,
}),
[menuState, coordinatedToggle, coordinatedShow]
);
}
+1 -2
View File
@@ -1,7 +1,7 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { MenuButton } from "reakit/Menu";
import { MenuButton, useMenuState } from "reakit/Menu";
import ContextMenu from "~/components/ContextMenu";
import Template from "~/components/ContextMenu/Template";
import {
@@ -16,7 +16,6 @@ import {
logout,
} from "~/actions/definitions/navigation";
import { changeTheme } from "~/actions/definitions/settings";
import { useMenuState } from "~/hooks/useMenuState";
import usePrevious from "~/hooks/usePrevious";
import useStores from "~/hooks/useStores";
import separator from "~/menus/separator";
+1 -1
View File
@@ -1,12 +1,12 @@
import { observer } from "mobx-react";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useMenuState } from "reakit/Menu";
import ApiKey from "~/models/ApiKey";
import ApiKeyRevokeDialog from "~/scenes/Settings/components/ApiKeyRevokeDialog";
import ContextMenu from "~/components/ContextMenu";
import MenuItem from "~/components/ContextMenu/MenuItem";
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
import { useMenuState } from "~/hooks/useMenuState";
import useStores from "~/hooks/useStores";
type Props = {
+1 -1
View File
@@ -1,8 +1,8 @@
import { useTranslation } from "react-i18next";
import { useMenuState } from "reakit/Menu";
import ContextMenu from "~/components/ContextMenu";
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
import Template from "~/components/ContextMenu/Template";
import { useMenuState } from "~/hooks/useMenuState";
import { MenuInternalLink } from "~/types";
type Props = {
+1 -1
View File
@@ -1,9 +1,9 @@
import { observer } from "mobx-react";
import { useTranslation } from "react-i18next";
import { useMenuState } from "reakit/Menu";
import ContextMenu from "~/components/ContextMenu";
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
import Template from "~/components/ContextMenu/Template";
import { useMenuState } from "~/hooks/useMenuState";
type Props = {
onMembers: () => void;
+4 -5
View File
@@ -1,4 +1,3 @@
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
import { observer } from "mobx-react";
import {
NewDocumentIcon,
@@ -12,7 +11,8 @@ import {
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import { MenuButton, MenuButtonHTMLProps } from "reakit/Menu";
import { useMenuState, MenuButton, MenuButtonHTMLProps } from "reakit/Menu";
import { VisuallyHidden } from "reakit/VisuallyHidden";
import { toast } from "sonner";
import { SubscriptionType } from "@shared/types";
import { getEventFiles } from "@shared/utils/files";
@@ -37,7 +37,6 @@ import {
} from "~/actions/definitions/collections";
import useActionContext from "~/hooks/useActionContext";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import { useMenuState } from "~/hooks/useMenuState";
import usePolicy from "~/hooks/usePolicy";
import useRequest from "~/hooks/useRequest";
import useStores from "~/hooks/useStores";
@@ -282,7 +281,7 @@ function CollectionMenu({
return (
<>
<VisuallyHidden.Root>
<VisuallyHidden>
<label>
{t("Import document")}
<input
@@ -294,7 +293,7 @@ function CollectionMenu({
tabIndex={-1}
/>
</label>
</VisuallyHidden.Root>
</VisuallyHidden>
{label ? (
<MenuButton {...menu} onPointerEnter={handlePointerEnter}>
{label}
+1 -1
View File
@@ -3,6 +3,7 @@ import { observer } from "mobx-react";
import { CopyIcon, EditIcon } from "outline-icons";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useMenuState } from "reakit/Menu";
import { toast } from "sonner";
import EventBoundary from "@shared/components/EventBoundary";
import Comment from "~/models/Comment";
@@ -17,7 +18,6 @@ import {
viewCommentReactionsFactory,
} from "~/actions/definitions/comments";
import useActionContext from "~/hooks/useActionContext";
import { useMenuState } from "~/hooks/useMenuState";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import { commentPath, urlify } from "~/utils/routeHelpers";
+19 -28
View File
@@ -1,4 +1,3 @@
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
import capitalize from "lodash/capitalize";
import isEmpty from "lodash/isEmpty";
import noop from "lodash/noop";
@@ -13,7 +12,8 @@ import {
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import { MenuButton, MenuButtonHTMLProps } from "reakit/Menu";
import { useMenuState, MenuButton, MenuButtonHTMLProps } from "reakit/Menu";
import { VisuallyHidden } from "reakit/VisuallyHidden";
import { toast } from "sonner";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
@@ -59,7 +59,6 @@ import {
import useActionContext from "~/hooks/useActionContext";
import useBoolean from "~/hooks/useBoolean";
import useCurrentUser from "~/hooks/useCurrentUser";
import { useMenuState } from "~/hooks/useMenuState";
import useMobile from "~/hooks/useMobile";
import usePolicy from "~/hooks/usePolicy";
import useRequest from "~/hooks/useRequest";
@@ -234,27 +233,6 @@ const MenuContent: React.FC<MenuContentProps> = observer(function MenuContent_({
onSelectTemplate,
});
const handleEmbedsToggle = React.useCallback(
(checked: boolean) => {
if (checked) {
document.enableEmbeds();
} else {
document.disableEmbeds();
}
},
[document]
);
const handleFullWidthToggle = React.useCallback(
(checked: boolean) => {
user.setPreference(UserPreference.FullWidthDocuments, checked);
void user.save();
document.fullWidth = checked;
void document.save({ fullWidth: checked });
},
[user, document]
);
return !isEmpty(can) ? (
<ContextMenu
{...menuState}
@@ -385,7 +363,11 @@ const MenuContent: React.FC<MenuContentProps> = observer(function MenuContent_({
label={t("Enable embeds")}
labelPosition="left"
checked={!document.embedsDisabled}
onChange={handleEmbedsToggle}
onChange={
document.embedsDisabled
? document.enableEmbeds
: document.disableEmbeds
}
/>
</Style>
)}
@@ -397,7 +379,16 @@ const MenuContent: React.FC<MenuContentProps> = observer(function MenuContent_({
label={t("Full width")}
labelPosition="left"
checked={document.fullWidth}
onChange={handleFullWidthToggle}
onChange={(ev) => {
const fullWidth = ev.currentTarget.checked;
user.setPreference(
UserPreference.FullWidthDocuments,
fullWidth
);
void user.save();
document.fullWidth = fullWidth;
void document.save();
}}
/>
</Style>
)}
@@ -477,7 +468,7 @@ function DocumentMenu({
return (
<>
<VisuallyHidden.Root>
<VisuallyHidden>
<label>
{t("Import document")}
<input
@@ -489,7 +480,7 @@ function DocumentMenu({
tabIndex={-1}
/>
</label>
</VisuallyHidden.Root>
</VisuallyHidden>
<MenuContext.Provider value={{ model: document, menuState }}>
<MenuTrigger label={label} onTrigger={showMenu} />
{isMenuVisible ? (
+1 -1
View File
@@ -1,12 +1,12 @@
import { DownloadIcon, TrashIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useMenuState } from "reakit/Menu";
import { FileOperationState, FileOperationType } from "@shared/types";
import FileOperation from "~/models/FileOperation";
import ContextMenu from "~/components/ContextMenu";
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
import Template from "~/components/ContextMenu/Template";
import { useMenuState } from "~/hooks/useMenuState";
import usePolicy from "~/hooks/usePolicy";
type Props = {
+1 -1
View File
@@ -1,9 +1,9 @@
import { observer } from "mobx-react";
import { useTranslation } from "react-i18next";
import { useMenuState } from "reakit/Menu";
import ContextMenu from "~/components/ContextMenu";
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
import Template from "~/components/ContextMenu/Template";
import { useMenuState } from "~/hooks/useMenuState";
type Props = {
onRemove: () => void;
+1 -1
View File
@@ -2,6 +2,7 @@ import { observer } from "mobx-react";
import { EditIcon, GroupIcon, TrashIcon } from "outline-icons";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useMenuState } from "reakit/Menu";
import Group from "~/models/Group";
import {
DeleteGroupDialog,
@@ -11,7 +12,6 @@ import {
import ContextMenu from "~/components/ContextMenu";
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
import Template from "~/components/ContextMenu/Template";
import { useMenuState } from "~/hooks/useMenuState";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
+1 -1
View File
@@ -2,11 +2,11 @@ import { observer } from "mobx-react";
import { CrossIcon, TrashIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useMenuState } from "reakit/Menu";
import Import from "~/models/Import";
import ContextMenu from "~/components/ContextMenu";
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
import Template from "~/components/ContextMenu/Template";
import { useMenuState } from "~/hooks/useMenuState";
import usePolicy from "~/hooks/usePolicy";
import { MenuItem } from "~/types";
+1 -2
View File
@@ -1,7 +1,7 @@
import { t } from "i18next";
import { MoreIcon } from "outline-icons";
import * as React from "react";
import { MenuButton } from "reakit/Menu";
import { MenuButton, useMenuState } from "reakit/Menu";
import styled from "styled-components";
import { s, hover } from "@shared/styles";
import ContextMenu from "~/components/ContextMenu";
@@ -10,7 +10,6 @@ import NudeButton from "~/components/NudeButton";
import { actionToMenuItem } from "~/actions";
import { toggleViewerInsights } from "~/actions/definitions/documents";
import useActionContext from "~/hooks/useActionContext";
import { useMenuState } from "~/hooks/useMenuState";
import { MenuItem } from "~/types";
const InsightsMenu: React.FC = () => {
+1 -1
View File
@@ -1,10 +1,10 @@
import { useTranslation } from "react-i18next";
import { useMenuState } from "reakit/Menu";
import User from "~/models/User";
import ContextMenu from "~/components/ContextMenu";
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
import Template from "~/components/ContextMenu/Template";
import useCurrentUser from "~/hooks/useCurrentUser";
import { useMenuState } from "~/hooks/useMenuState";
type Props = {
user: User;
+1 -2
View File
@@ -1,11 +1,10 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import { MenuButton, MenuButtonHTMLProps } from "reakit/Menu";
import { useMenuState, MenuButton, MenuButtonHTMLProps } from "reakit/Menu";
import Document from "~/models/Document";
import ContextMenu from "~/components/ContextMenu";
import Template from "~/components/ContextMenu/Template";
import { useMenuState } from "~/hooks/useMenuState";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import { MenuItem } from "~/types";
+1 -2
View File
@@ -2,14 +2,13 @@ import { observer } from "mobx-react";
import { PlusIcon } from "outline-icons";
import { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { MenuButton } from "reakit/Menu";
import { MenuButton, useMenuState } from "reakit/Menu";
import Button from "~/components/Button";
import ContextMenu from "~/components/ContextMenu";
import Template from "~/components/ContextMenu/Template";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import TeamLogo from "~/components/TeamLogo";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import { useMenuState } from "~/hooks/useMenuState";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import { MenuItem } from "~/types";
+1 -2
View File
@@ -1,7 +1,7 @@
import { t } from "i18next";
import { MoreIcon } from "outline-icons";
import * as React from "react";
import { MenuButton } from "reakit/Menu";
import { MenuButton, useMenuState } from "reakit/Menu";
import styled from "styled-components";
import { s, hover } from "@shared/styles";
import ContextMenu from "~/components/ContextMenu";
@@ -11,7 +11,6 @@ import { actionToMenuItem, performAction } from "~/actions";
import { navigateToNotificationSettings } from "~/actions/definitions/navigation";
import { markNotificationsAsArchived } from "~/actions/definitions/notifications";
import useActionContext from "~/hooks/useActionContext";
import { useMenuState } from "~/hooks/useMenuState";
import useOnClickOutside from "~/hooks/useOnClickOutside";
import { MenuItem } from "~/types";
+1 -1
View File
@@ -1,12 +1,12 @@
import { observer } from "mobx-react";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useMenuState } from "reakit/Menu";
import OAuthAuthentication from "~/models/oauth/OAuthAuthentication";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import ContextMenu from "~/components/ContextMenu";
import MenuItem from "~/components/ContextMenu/MenuItem";
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
import { useMenuState } from "~/hooks/useMenuState";
import useStores from "~/hooks/useStores";
type Props = {
+1 -1
View File
@@ -1,12 +1,12 @@
import { observer } from "mobx-react";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useMenuState } from "reakit/Menu";
import OAuthClient from "~/models/oauth/OAuthClient";
import OAuthClientDeleteDialog from "~/scenes/Settings/components/OAuthClientDeleteDialog";
import ContextMenu from "~/components/ContextMenu";
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
import Template from "~/components/ContextMenu/Template";
import { useMenuState } from "~/hooks/useMenuState";
import useStores from "~/hooks/useStores";
import { settingsPath } from "~/utils/routeHelpers";
+1 -1
View File
@@ -1,5 +1,6 @@
import { observer } from "mobx-react";
import { useTranslation } from "react-i18next";
import { useMenuState } from "reakit/Menu";
import Document from "~/models/Document";
import ContextMenu from "~/components/ContextMenu";
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
@@ -10,7 +11,6 @@ import {
restoreRevision,
} from "~/actions/definitions/revisions";
import useActionContext from "~/hooks/useActionContext";
import { useMenuState } from "~/hooks/useMenuState";
import separator from "./separator";
type Props = {
+1 -1
View File
@@ -3,13 +3,13 @@ import { ArrowIcon, CopyIcon, TrashIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import { useMenuState } from "reakit/Menu";
import { toast } from "sonner";
import Share from "~/models/Share";
import ContextMenu from "~/components/ContextMenu";
import MenuItem from "~/components/ContextMenu/MenuItem";
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
import CopyToClipboard from "~/components/CopyToClipboard";
import { useMenuState } from "~/hooks/useMenuState";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
+1 -2
View File
@@ -2,13 +2,12 @@ import { observer } from "mobx-react";
import { TableOfContentsIcon } from "outline-icons";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { MenuButton } from "reakit/Menu";
import { MenuButton, useMenuState } from "reakit/Menu";
import styled from "styled-components";
import Button from "~/components/Button";
import ContextMenu from "~/components/ContextMenu";
import Template from "~/components/ContextMenu/Template";
import { useDocumentContext } from "~/components/DocumentContext";
import { useMenuState } from "~/hooks/useMenuState";
import { MenuItem } from "~/types";
function TableOfContentsMenu() {
+1 -2
View File
@@ -1,7 +1,7 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { MenuButton } from "reakit/Menu";
import { MenuButton, useMenuState } from "reakit/Menu";
import ContextMenu from "~/components/ContextMenu";
import Template from "~/components/ContextMenu/Template";
import {
@@ -14,7 +14,6 @@ import {
desktopLoginTeam,
} from "~/actions/definitions/teams";
import useActionContext from "~/hooks/useActionContext";
import { useMenuState } from "~/hooks/useMenuState";
import usePrevious from "~/hooks/usePrevious";
import useStores from "~/hooks/useStores";
import separator from "~/menus/separator";
+1 -2
View File
@@ -1,12 +1,11 @@
import { observer } from "mobx-react";
import { ShapesIcon } from "outline-icons";
import { useTranslation } from "react-i18next";
import { MenuButton } from "reakit/Menu";
import { MenuButton, useMenuState } from "reakit/Menu";
import Document from "~/models/Document";
import Button from "~/components/Button";
import ContextMenu from "~/components/ContextMenu";
import Template from "~/components/ContextMenu/Template";
import { useMenuState } from "~/hooks/useMenuState";
import { useTemplateMenuItems } from "~/hooks/useTemplateMenuItems";
type Props = {
+1 -1
View File
@@ -1,6 +1,7 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useMenuState } from "reakit/Menu";
import { toast } from "sonner";
import { UserRole } from "@shared/types";
import User from "~/models/User";
@@ -18,7 +19,6 @@ import {
updateUserRoleActionFactory,
} from "~/actions/definitions/users";
import useActionContext from "~/hooks/useActionContext";
import { useMenuState } from "~/hooks/useMenuState";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
-23
View File
@@ -6,7 +6,6 @@ import {
type NavigationNode,
NavigationNodeType,
type ProsemirrorData,
TeamPreference,
} from "@shared/types";
import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper";
import { sortNavigationNodes } from "@shared/utils/collections";
@@ -69,13 +68,6 @@ export default class Collection extends ParanoidModel {
direction: "asc" | "desc";
};
/**
* Whether commenting is enabled for the collection.
*/
@Field
@observable
commenting?: boolean | null;
/** The child documents of the collection. */
@observable
documents?: NavigationNode[];
@@ -129,21 +121,6 @@ export default class Collection extends ParanoidModel {
return !this.permission;
}
/**
* Returns whether comments should be enabled for this collection,
*
* @returns boolean
*/
@computed
get canCreateComment(): boolean {
const teamCommentingEnabled =
!!this.store.rootStore.auth.team?.getPreference(
TeamPreference.Commenting
);
return teamCommentingEnabled && this.commenting !== false;
}
/** Returns whether the collection description is not empty. */
@computed
get hasDescription(): boolean {
-4
View File
@@ -60,10 +60,6 @@ class Share extends Model implements Searchable {
@observable
allowIndexing: boolean;
@Field
@observable
showLastUpdated: boolean;
@observable
views: number;
-4
View File
@@ -12,10 +12,6 @@ class Team extends Model {
@observable
name: string;
@Field
@observable
description: string | null;
@Field
@observable
avatarUrl: string;
-5
View File
@@ -195,11 +195,6 @@ function SharedDocumentScene(props: Props) {
rel="canonical"
href={canonicalOrigin + location.pathname.replace(/\/$/, "")}
/>
<link
rel="sitemap"
type="application/xml"
href={`${env.URL}/api/documents.sitemap?shareId=${shareId}`}
/>
</Helmet>
<TeamContext.Provider value={response.team}>
<ThemeProvider theme={theme}>
@@ -1,10 +1,10 @@
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
import { m } from "framer-motion";
import { action } from "mobx";
import { observer } from "mobx-react";
import { ImageIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { VisuallyHidden } from "reakit";
import { toast } from "sonner";
import { useTheme } from "styled-components";
import { v4 as uuidv4 } from "uuid";
@@ -266,7 +266,7 @@ function CommentForm({
{...presence}
{...rest}
>
<VisuallyHidden.Root>
<VisuallyHidden>
<input
ref={file}
type="file"
@@ -274,7 +274,7 @@ function CommentForm({
accept={AttachmentValidation.imageContentTypes.join(", ")}
tabIndex={-1}
/>
</VisuallyHidden.Root>
</VisuallyHidden>
<Flex gap={8} align="flex-start" reverse={dir === "rtl"}>
<Avatar model={user} size={24} style={{ marginTop: 8 }} />
<Bubble
@@ -165,7 +165,7 @@ function CommentThreadItem({
setReadOnly();
comment.data = data;
await comment.save();
} catch (_err) {
} catch (error) {
setEditing();
toast.error(t("Error updating comment"));
}
@@ -294,7 +294,7 @@ const ResolveButton = ({
const { t } = useTranslation();
return (
<Tooltip content={t("Mark as resolved")} placement="top">
<Tooltip content={t("Mark as resolved")} placement="top" hideOnClick>
<Action
as={NudeButton}
context={context}
@@ -25,7 +25,7 @@ type Props = {
};
function TitleDocumentMeta({ to, document, revision, ...rest }: Props) {
const { collections, views, comments, ui } = useStores();
const { views, comments, ui } = useStores();
const { t } = useTranslation();
const match = useRouteMatch();
const sidebarContext = useLocationSidebarContext();
@@ -41,16 +41,9 @@ function TitleDocumentMeta({ to, document, revision, ...rest }: Props) {
const insightsPath = documentInsightsPath(document);
const commentsCount = comments.unresolvedCommentsInDocumentCount(document.id);
const collection = document.collectionId
? collections.get(document.collectionId)
: undefined;
const collectionCommentingEnabled =
collection?.canCreateComment ??
!!team.getPreference(TeamPreference.Commenting);
return (
<Meta document={document} revision={revision} to={to} replace {...rest}>
{collectionCommentingEnabled && can.comment && (
{team.getPreference(TeamPreference.Commenting) && can.comment && (
<>
&nbsp;&nbsp;
<CommentLink
+11 -38
View File
@@ -4,8 +4,6 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import { mergeRefs } from "react-merge-refs";
import { useHistory, useRouteMatch } from "react-router-dom";
import styled from "styled-components";
import Text from "@shared/components/Text";
import { richExtensions, withComments } from "@shared/editor/nodes";
import { TeamPreference } from "@shared/types";
import { colorPalette } from "@shared/utils/collections";
@@ -15,7 +13,6 @@ import { RefHandle } from "~/components/ContentEditable";
import { useDocumentContext } from "~/components/DocumentContext";
import Editor, { Props as EditorProps } from "~/components/Editor";
import Flex from "~/components/Flex";
import Time from "~/components/Time";
import { withUIExtensions } from "~/editor/extensions";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useCurrentUser from "~/hooks/useCurrentUser";
@@ -60,7 +57,7 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
const { t } = useTranslation();
const match = useRouteMatch();
const focusedComment = useFocusedComment();
const { ui, comments, collections } = useStores();
const { ui, comments } = useStores();
const user = useCurrentUser({ rejectOnEmpty: false });
const team = useCurrentTeam({ rejectOnEmpty: false });
const history = useHistory();
@@ -78,15 +75,6 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
...rest
} = props;
const can = usePolicy(document);
// Check collection-level commenting setting
const collection = document.collectionId
? collections.get(document.collectionId)
: undefined;
const collectionCommentingEnabled =
collection?.canCreateComment ??
!!team?.getPreference(TeamPreference.Commenting);
const iconColor = document.color ?? (last(colorPalette) as string);
const childRef = React.useRef<HTMLDivElement>(null);
const focusAtStart = React.useCallback(() => {
@@ -232,26 +220,16 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
onBlur={handleBlur}
placeholder={t("Untitled")}
/>
{shareId ? (
document.updatedAt ? (
<SharedMeta type="tertiary">
{t("Last updated")} <Time dateTime={document.updatedAt} addSuffix />
</SharedMeta>
) : null
) : (
{!shareId && (
<DocumentMeta
document={document}
to={
shareId
? undefined
: {
pathname:
match.path === matchDocumentHistory
? documentPath(document)
: documentHistoryPath(document),
state: { sidebarContext },
}
}
to={{
pathname:
match.path === matchDocumentHistory
? documentPath(document)
: documentHistoryPath(document),
state: { sidebarContext },
}}
rtl={direction === "rtl"}
/>
)}
@@ -266,12 +244,12 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
focusedCommentId={focusedComment?.id}
onClickCommentMark={handleClickComment}
onCreateCommentMark={
collectionCommentingEnabled && can.comment
team?.getPreference(TeamPreference.Commenting) && can.comment
? handleDraftComment
: undefined
}
onDeleteCommentMark={
collectionCommentingEnabled && can.comment
team?.getPreference(TeamPreference.Commenting) && can.comment
? handleRemoveComment
: undefined
}
@@ -287,9 +265,4 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
);
}
const SharedMeta = styled(Text)`
margin: -12px 0 2em 0;
font-size: 14px;
`;
export default observer(React.forwardRef(DocumentEditor));
@@ -21,9 +21,7 @@ function MarkAsViewed(props: Props) {
}, MARK_AS_VIEWED_AFTER);
return () => {
if (viewTimeout.current) {
clearTimeout(viewTimeout.current);
}
viewTimeout.current && clearTimeout(viewTimeout.current);
};
}, [document]);
@@ -45,7 +45,6 @@ function RevisionViewer(props: Props) {
dangerouslySetInnerHTML={{ __html: revision.html }}
dir={revision.dir}
rtl={revision.rtl}
readOnly
/>
{children}
</Flex>
+1 -1
View File
@@ -86,7 +86,7 @@ function DocumentMove({ document }: Props) {
toast.success(t("Document moved"));
dialogs.closeAllModals();
} catch (_err) {
} catch (err) {
toast.error(t("Couldnt move the document, try again?"));
}
};

Some files were not shown because too many files have changed in this diff Show More