chore: Refactor useActionContext to use React context (#10140)

* chore: Refactor useActionContext to use React context

* Self review

* PR feedback
This commit is contained in:
Tom Moor
2025-09-10 01:22:46 +02:00
committed by GitHub
parent be194558bf
commit 1da18c3101
38 changed files with 333 additions and 352 deletions

View File

@@ -27,8 +27,8 @@ export const createApiKey = createAction({
export const revokeApiKeyFactory = ({ apiKey }: { apiKey: ApiKey }) =>
createActionV2({
name: ({ t, isContextMenu }) =>
isContextMenu
name: ({ t, isMenu }) =>
isMenu
? apiKey.isExpired
? t("Delete")
: `${t("Revoke")}`

View File

@@ -81,8 +81,7 @@ export const createCollection = createAction({
});
export const editCollection = createActionV2({
name: ({ t, isContextMenu }) =>
isContextMenu ? `${t("Edit")}` : t("Edit collection"),
name: ({ t, isMenu }) => (isMenu ? `${t("Edit")}` : t("Edit collection")),
analyticsName: "Edit collection",
section: ActiveCollectionSection,
icon: <EditIcon />,
@@ -107,8 +106,8 @@ export const editCollection = createActionV2({
});
export const editCollectionPermissions = createActionV2({
name: ({ t, isContextMenu }) =>
isContextMenu ? `${t("Permissions")}` : t("Collection permissions"),
name: ({ t, isMenu }) =>
isMenu ? `${t("Permissions")}` : t("Collection permissions"),
analyticsName: "Collection permissions",
section: ActiveCollectionSection,
icon: <PadlockIcon />,

View File

@@ -384,8 +384,8 @@ export const subscribeDocument = createActionV2({
analyticsName: "Subscribe to document",
section: ActiveDocumentSection,
icon: <SubscribeIcon />,
tooltip: ({ activeCollectionId, isContextMenu, stores, t }) => {
if (!isContextMenu || !activeCollectionId) {
tooltip: ({ activeCollectionId, isMenu, stores, t }) => {
if (!isMenu || !activeCollectionId) {
return undefined;
}
@@ -393,8 +393,8 @@ export const subscribeDocument = createActionV2({
? t("Subscription inherited from collection")
: undefined;
},
disabled: ({ activeCollectionId, isContextMenu, stores }) => {
if (!isContextMenu || !activeCollectionId) {
disabled: ({ activeCollectionId, isMenu, stores }) => {
if (!isMenu || !activeCollectionId) {
return false;
}
@@ -430,8 +430,8 @@ export const unsubscribeDocument = createActionV2({
analyticsName: "Unsubscribe from document",
section: ActiveDocumentSection,
icon: <UnsubscribeIcon />,
tooltip: ({ activeCollectionId, isContextMenu, stores, t }) => {
if (!isContextMenu || !activeCollectionId) {
tooltip: ({ activeCollectionId, isMenu, stores, t }) => {
if (!isMenu || !activeCollectionId) {
return undefined;
}
@@ -439,8 +439,8 @@ export const unsubscribeDocument = createActionV2({
? t("Subscription inherited from collection")
: undefined;
},
disabled: ({ activeCollectionId, isContextMenu, stores }) => {
if (!isContextMenu || !activeCollectionId) {
disabled: ({ activeCollectionId, isMenu, stores }) => {
if (!isMenu || !activeCollectionId) {
return false;
}
@@ -571,8 +571,7 @@ export const downloadDocumentAsMarkdown = createActionV2({
});
export const downloadDocument = createActionV2WithChildren({
name: ({ t, isContextMenu }) =>
isContextMenu ? t("Download") : t("Download document"),
name: ({ t, isMenu }) => (isMenu ? t("Download") : t("Download document")),
analyticsName: "Download document",
section: ActiveDocumentSection,
icon: <DownloadIcon />,
@@ -678,8 +677,7 @@ export const copyDocument = createActionV2WithChildren({
});
export const duplicateDocument = createActionV2({
name: ({ t, isContextMenu }) =>
isContextMenu ? t("Duplicate") : t("Duplicate document"),
name: ({ t, isMenu }) => (isMenu ? t("Duplicate") : t("Duplicate document")),
analyticsName: "Duplicate document",
section: ActiveDocumentSection,
icon: <DuplicateIcon />,
@@ -829,8 +827,7 @@ export const searchInDocument = createInternalLinkActionV2({
});
export const printDocument = createActionV2({
name: ({ t, isContextMenu }) =>
isContextMenu ? t("Print") : t("Print document"),
name: ({ t, isMenu }) => (isMenu ? t("Print") : t("Print document")),
analyticsName: "Print document",
section: ActiveDocumentSection,
icon: <PrintIcon />,

View File

@@ -131,8 +131,8 @@ export const navigateToTemplateSettings = createAction({
});
export const navigateToNotificationSettings = createInternalLinkActionV2({
name: ({ t, isContextMenu }) =>
isContextMenu ? t("Notification settings") : t("Notifications"),
name: ({ t, isMenu }) =>
isMenu ? t("Notification settings") : t("Notifications"),
analyticsName: "Navigate to notification settings",
section: NavigationSection,
iconInContextMenu: false,

View File

@@ -37,8 +37,7 @@ export const changeToSystemTheme = createActionV2({
});
export const changeTheme = createActionV2WithChildren({
name: ({ t, isContextMenu }) =>
isContextMenu ? t("Appearance") : t("Change theme"),
name: ({ t, isMenu }) => (isMenu ? t("Appearance") : t("Change theme")),
analyticsName: "Change theme",
placeholder: ({ t }) => t("Change theme to"),
icon: ({ stores }) =>

View File

@@ -3,12 +3,8 @@ import * as React from "react";
import Tooltip, { Props as TooltipProps } from "~/components/Tooltip";
import { performAction, performActionV2, resolve } from "~/actions";
import useIsMounted from "~/hooks/useIsMounted";
import {
Action,
ActionContext,
ActionV2Variant,
ActionV2WithChildren,
} from "~/types";
import { Action, ActionV2Variant, ActionV2WithChildren } from "~/types";
import useActionContext from "~/hooks/useActionContext";
export type Props = React.HTMLAttributes<HTMLButtonElement> & {
/** Show the button in a disabled state */
@@ -17,8 +13,6 @@ export type Props = React.HTMLAttributes<HTMLButtonElement> & {
hideOnActionDisabled?: boolean;
/** Action to use on button */
action?: Action | Exclude<ActionV2Variant, ActionV2WithChildren>;
/** Context of action, must be provided with action */
context?: ActionContext;
/** If tooltip props are provided the button will be wrapped in a tooltip */
tooltip?: Omit<TooltipProps, "children">;
};
@@ -28,22 +22,20 @@ export type Props = React.HTMLAttributes<HTMLButtonElement> & {
*/
const ActionButton = React.forwardRef<HTMLButtonElement, Props>(
function _ActionButton(
{ action, context, tooltip, hideOnActionDisabled, ...rest }: Props,
{ action, tooltip, hideOnActionDisabled, ...rest }: Props,
ref: React.Ref<HTMLButtonElement>
) {
const actionContext = useActionContext({
isButton: true,
});
const isMounted = useIsMounted();
const [executing, setExecuting] = React.useState(false);
const disabled = rest.disabled;
if (action && !context) {
throw new Error("Context must be provided with action");
}
if (!context || !action) {
if (!actionContext || !action) {
return <button {...rest} ref={ref} />;
}
const actionContext = { ...context, isButton: true };
if (
action.visible &&
!resolve<boolean>(action.visible, actionContext) &&

View File

@@ -25,7 +25,7 @@ function Breadcrumb(
{ actions, highlightFirstItem, children, max = 2 }: Props,
ref: React.RefObject<HTMLDivElement> | null
) {
const actionContext = useActionContext({ isContextMenu: true });
const actionContext = useActionContext({ isMenu: true });
const visibleActions = useComputed(
() =>

View File

@@ -104,7 +104,7 @@ export function filterTemplateItems(items: TMenuItem[]): TMenuItem[] {
function Template({ items, actions, context, showIcons, ...menu }: Props) {
const ctx = useActionContext({
isContextMenu: true,
isMenu: true,
});
const templateItems = actions

View File

@@ -25,7 +25,7 @@ import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import DocumentMenu from "~/menus/DocumentMenu";
import { documentPath } from "~/utils/routeHelpers";
import { determineSidebarContext } from "./Sidebar/components/SidebarContext";
import useActionContext from "~/hooks/useActionContext";
import { ActionContextProvider } from "~/hooks/useActionContext";
import { useDocumentMenuAction } from "~/hooks/useDocumentMenuAction";
import { ContextMenu } from "./Menu/ContextMenu";
import useStores from "~/hooks/useStores";
@@ -94,97 +94,99 @@ function DocumentListItem(
currentContext: locationSidebarContext,
});
const actionContext = useActionContext({
isContextMenu: true,
activeDocumentId: document.id,
activeCollectionId:
!isShared && document.collectionId ? document.collectionId : undefined,
});
const contextMenuAction = useDocumentMenuAction({ document });
return (
<ContextMenu
action={contextMenuAction}
context={actionContext}
ariaLabel={t("Document options")}
onOpen={handleMenuOpen}
onClose={handleMenuClose}
<ActionContextProvider
value={{
activeDocumentId: document.id,
activeCollectionId:
!isShared && document.collectionId
? document.collectionId
: undefined,
}}
>
<DocumentLink
ref={itemRef}
dir={document.dir}
$isStarred={document.isStarred}
$menuOpen={menuOpen}
to={{
pathname: documentPath(document),
state: {
title: document.titleWithDefault,
sidebarContext,
},
}}
{...rest}
{...rovingTabIndex}
<ContextMenu
action={contextMenuAction}
ariaLabel={t("Document options")}
onOpen={handleMenuOpen}
onClose={handleMenuClose}
>
<Content>
<Heading dir={document.dir}>
{document.icon && (
<>
<Icon
value={document.icon}
color={document.color ?? undefined}
initial={document.initial}
/>
&nbsp;
</>
)}
<Title
text={document.titleWithDefault}
highlight={highlight}
dir={document.dir}
/>
{document.isBadgedNew && document.createdBy?.id !== user.id && (
<Badge yellow>{t("New")}</Badge>
)}
{document.isDraft && showDraft && (
<Tooltip content={t("Only visible to you")} placement="top">
<Badge>{t("Draft")}</Badge>
</Tooltip>
)}
{canStar && (
<StarPositioner>
<StarButton document={document} />
</StarPositioner>
)}
{document.isTemplate && showTemplate && (
<Badge primary>{t("Template")}</Badge>
)}
</Heading>
<DocumentLink
ref={itemRef}
dir={document.dir}
$isStarred={document.isStarred}
$menuOpen={menuOpen}
to={{
pathname: documentPath(document),
state: {
title: document.titleWithDefault,
sidebarContext,
},
}}
{...rest}
{...rovingTabIndex}
>
<Content>
<Heading dir={document.dir}>
{document.icon && (
<>
<Icon
value={document.icon}
color={document.color ?? undefined}
initial={document.initial}
/>
&nbsp;
</>
)}
<Title
text={document.titleWithDefault}
highlight={highlight}
dir={document.dir}
/>
{document.isBadgedNew && document.createdBy?.id !== user.id && (
<Badge yellow>{t("New")}</Badge>
)}
{document.isDraft && showDraft && (
<Tooltip content={t("Only visible to you")} placement="top">
<Badge>{t("Draft")}</Badge>
</Tooltip>
)}
{canStar && (
<StarPositioner>
<StarButton document={document} />
</StarPositioner>
)}
{document.isTemplate && showTemplate && (
<Badge primary>{t("Template")}</Badge>
)}
</Heading>
{!queryIsInTitle && (
<ResultContext
text={context}
highlight={highlight ? SEARCH_RESULT_REGEX : undefined}
processResult={replaceResultMarks}
{!queryIsInTitle && (
<ResultContext
text={context}
highlight={highlight ? SEARCH_RESULT_REGEX : undefined}
processResult={replaceResultMarks}
/>
)}
<DocumentMeta
document={document}
showCollection={showCollection}
showPublished={showPublished}
showParentDocuments={showParentDocuments}
showLastViewed
/>
)}
<DocumentMeta
document={document}
showCollection={showCollection}
showPublished={showPublished}
showParentDocuments={showParentDocuments}
showLastViewed
/>
</Content>
<Actions>
<DocumentMenu
document={document}
onOpen={handleMenuOpen}
onClose={handleMenuClose}
/>
</Actions>
</DocumentLink>
</ContextMenu>
</Content>
<Actions>
<DocumentMenu
document={document}
onOpen={handleMenuOpen}
onClose={handleMenuClose}
/>
</Actions>
</DocumentLink>
</ContextMenu>
</ActionContextProvider>
);
}

View File

@@ -2,7 +2,7 @@ import * as React from "react";
import { actionV2ToMenuItem } from "~/actions";
import useActionContext from "~/hooks/useActionContext";
import useMobile from "~/hooks/useMobile";
import { ActionContext, ActionV2Variant, ActionV2WithChildren } from "~/types";
import { ActionV2Variant, ActionV2WithChildren } from "~/types";
import { toMenuItems } from "./transformer";
import { observer } from "mobx-react";
import { useComputed } from "~/hooks/useComputed";
@@ -12,8 +12,6 @@ import { MenuProvider } from "~/components/primitives/Menu/MenuContext";
type Props = {
/** Root action with children representing the menu items */
action: ActionV2WithChildren;
/** Action context to use - new context will be created if not provided */
context?: ActionContext;
/** Trigger for the menu */
children: React.ReactNode;
/** ARIA label for the menu */
@@ -25,15 +23,12 @@ type Props = {
};
export const ContextMenu = observer(
({ action, children, ariaLabel, context, onOpen, onClose }: Props) => {
({ action, children, ariaLabel, onOpen, onClose }: Props) => {
const isMobile = useMobile();
const contentRef = React.useRef<React.ElementRef<typeof MenuContent>>(null);
const actionContext =
context ??
useActionContext({
isContextMenu: true,
});
const actionContext = useActionContext({
isMenu: true,
});
const menuItems = useComputed(() => {
if (!open) {

View File

@@ -14,7 +14,6 @@ import { actionV2ToMenuItem } from "~/actions";
import useActionContext from "~/hooks/useActionContext";
import useMobile from "~/hooks/useMobile";
import {
ActionContext,
ActionV2Variant,
ActionV2WithChildren,
MenuItem,
@@ -27,8 +26,6 @@ import { useComputed } from "~/hooks/useComputed";
type Props = {
/** Root action with children representing the menu items */
action: ActionV2WithChildren;
/** Action context to use - new context will be created if not provided */
context?: ActionContext;
/** Trigger for the menu */
children: React.ReactNode;
/** Alignment w.r.t trigger - defaults to start */
@@ -49,7 +46,6 @@ export const DropdownMenu = observer(
(
{
action,
context,
children,
align = "start",
ariaLabel,
@@ -64,12 +60,9 @@ export const DropdownMenu = observer(
const isMobile = useMobile();
const contentRef =
React.useRef<React.ElementRef<typeof MenuContent>>(null);
const actionContext =
context ??
useActionContext({
isContextMenu: true,
});
const actionContext = useActionContext({
isMenu: true,
});
const menuItems = useComputed(() => {
if (!open) {

View File

@@ -6,7 +6,6 @@ import styled from "styled-components";
import { s, hover } from "@shared/styles";
import Notification from "~/models/Notification";
import { markNotificationsAsRead } from "~/actions/definitions/notifications";
import useActionContext from "~/hooks/useActionContext";
import useStores from "~/hooks/useStores";
import NotificationMenu from "~/menus/NotificationMenu";
import Desktop from "~/utils/Desktop";
@@ -32,7 +31,6 @@ function Notifications(
{ onRequestClose }: Props,
ref: React.RefObject<HTMLDivElement>
) {
const context = useActionContext();
const { notifications } = useStores();
const { t } = useTranslation();
const isEmpty = notifications.active.length === 0;
@@ -69,7 +67,6 @@ function Notifications(
<Tooltip content={t("Mark all as read")}>
<Button
action={markNotificationsAsRead}
context={context}
aria-label={t("Mark all as read")}
>
<MarkAsReadIcon />

View File

@@ -6,7 +6,6 @@ import { Inner } from "~/components/Button";
import ButtonSmall from "~/components/ButtonSmall";
import Fade from "~/components/Fade";
import InputMemberPermissionSelect from "~/components/InputMemberPermissionSelect";
import useActionContext from "~/hooks/useActionContext";
import { Action, Permission } from "~/types";
export function PermissionAction({
@@ -21,7 +20,6 @@ export function PermissionAction({
onChange: (permission: CollectionPermission | DocumentPermission) => void;
}) {
const { t } = useTranslation();
const context = useActionContext();
return (
<Fade timing="150ms" key="invite">
@@ -31,9 +29,7 @@ export function PermissionAction({
onChange={onChange}
value={permission}
/>
<ButtonSmall action={action} context={context}>
{t("Add")}
</ButtonSmall>
<ButtonSmall action={action}>{t("Add")}</ButtonSmall>
</Flex>
</Fade>
);

View File

@@ -12,7 +12,7 @@ type Props = {
function SidebarAction({ action, ...rest }: Props) {
const context = useActionContext({
isContextMenu: false,
isMenu: false,
isCommandBar: false,
activeCollectionId: undefined,
activeDocumentId: undefined,

View File

@@ -10,7 +10,7 @@ import {
unstarCollection,
} from "~/actions/definitions/collections";
import { starDocument, unstarDocument } from "~/actions/definitions/documents";
import useActionContext from "~/hooks/useActionContext";
import { ActionContextProvider } from "~/hooks/useActionContext";
import NudeButton from "./NudeButton";
type Props = {
@@ -27,10 +27,6 @@ type Props = {
function Star({ size, document, collection, color, ...rest }: Props) {
const { t } = useTranslation();
const theme = useTheme();
const context = useActionContext({
activeDocumentId: document?.id,
activeCollectionId: collection?.id,
});
const target = document || collection;
@@ -39,37 +35,43 @@ function Star({ size, document, collection, color, ...rest }: Props) {
}
return (
<NudeButton
context={context}
hideOnActionDisabled
tooltip={{
content: target.isStarred ? t("Unstar document") : t("Star document"),
delay: 500,
<ActionContextProvider
value={{
activeDocumentId: document?.id,
activeCollectionId: collection?.id,
}}
action={
collection
? collection.isStarred
? unstarCollection
: starCollection
: document
? document.isStarred
? unstarDocument
: starDocument
: undefined
}
size={size}
{...rest}
>
{target.isStarred ? (
<AnimatedStar size={size} color={theme.yellow} />
) : (
<AnimatedStar
size={size}
color={color ?? theme.textTertiary}
as={UnstarredIcon}
/>
)}
</NudeButton>
<NudeButton
hideOnActionDisabled
tooltip={{
content: target.isStarred ? t("Unstar document") : t("Star document"),
delay: 500,
}}
action={
collection
? collection.isStarred
? unstarCollection
: starCollection
: document
? document.isStarred
? unstarDocument
: starDocument
: undefined
}
size={size}
{...rest}
>
{target.isStarred ? (
<AnimatedStar size={size} color={theme.yellow} />
) : (
<AnimatedStar
size={size}
color={color ?? theme.textTertiary}
as={UnstarredIcon}
/>
)}
</NudeButton>
</ActionContextProvider>
);
}

View File

@@ -1,33 +0,0 @@
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router";
import useStores from "~/hooks/useStores";
import { ActionContext } from "~/types";
/**
* Hook to get the current action context, an object that is passed to all
* action definitions.
*
* @param overrides Overides of the default action context.
* @returns The current action context.
*/
export default function useActionContext(
overrides?: Partial<ActionContext>
): ActionContext {
const stores = useStores();
const { t } = useTranslation();
const location = useLocation();
return {
isContextMenu: false,
isCommandBar: false,
isButton: false,
activeCollectionId: stores.ui.activeCollectionId ?? undefined,
activeDocumentId: stores.ui.activeDocumentId,
currentUserId: stores.auth.user?.id,
currentTeamId: stores.auth.team?.id,
...overrides,
location,
stores,
t,
};
}

View File

@@ -0,0 +1,102 @@
import { observer } from "mobx-react";
import React, { createContext, useContext, ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router";
import useStores from "~/hooks/useStores";
import { ActionContext as ActionContextType } from "~/types";
export const ActionContext = createContext<ActionContextType | undefined>(
undefined
);
type ActionContextProviderProps = {
children: ReactNode;
value?: Partial<ActionContextType>;
};
/**
* Provider that allows overriding the action context at different levels
* of the React component tree.
*
* @example
* ```tsx
* // Override context for a command bar
* <ActionContextProvider value={{ isCommandBar: true }}>
* <CommandBar />
* </ActionContextProvider>
*
* // Nested overrides
* <ActionContextProvider value={{ activeCollectionId: "collection-1" }}>
* <CollectionView />
* <ActionContextProvider value={{ activeDocumentId: "doc-1" }}>
* <DocumentView />
* </ActionContextProvider>
* </ActionContextProvider>
* ```
*/
export const ActionContextProvider = observer(function ActionContextProvider_({
children,
value = {},
}: ActionContextProviderProps) {
const parentContext = useContext(ActionContext);
const stores = useStores();
const { t } = useTranslation();
const location = useLocation();
// Create the base context if we don't have a parent context
const baseContext: ActionContextType = parentContext ?? {
isMenu: false,
isCommandBar: false,
isButton: false,
activeCollectionId: stores.ui.activeCollectionId ?? undefined,
activeDocumentId: stores.ui.activeDocumentId ?? undefined,
currentUserId: stores.auth.user?.id,
currentTeamId: stores.auth.team?.id,
location,
stores,
t,
};
// Merge the parent context with the provided overrides
const contextValue: ActionContextType = {
...baseContext,
...value,
};
return (
<ActionContext.Provider value={contextValue}>
{children}
</ActionContext.Provider>
);
});
/**
* Hook to get the current action context, an object that is passed to all
* action definitions.
*
* This hook respects the ActionContextProvider hierarchy, merging values from:
* 1. Default system context (stores, location, translation)
* 2. Parent ActionContextProvider values (if any)
* 3. Local overrides parameter (highest priority)
*
* @param overrides Optional overrides of the action context. These will be
* merged with any provider context and take highest priority.
* @returns The current action context with all overrides applied.
*/
export default function useActionContext(
overrides?: Partial<ActionContextType>
): ActionContextType {
const contextValue = useContext(ActionContext);
// If we have a context value from a provider, use it as the base
if (contextValue) {
return {
...contextValue,
...overrides,
};
}
throw new Error(
"useActionContext must be used within an ActionContextProvider"
);
}

View File

@@ -25,6 +25,7 @@ import Logger from "./utils/Logger";
import { PluginManager } from "./utils/PluginManager";
import history from "./utils/history";
import { initSentry } from "./utils/sentry";
import { ActionContextProvider } from "./hooks/useActionContext";
// Load plugins as soon as possible
void PluginManager.loadPlugins();
@@ -53,25 +54,27 @@ if (element) {
<Provider {...stores}>
<Analytics>
<Theme>
<ErrorBoundary showTitle>
<KBarProvider actions={[]} options={commandBarOptions}>
<LazyPolyfill>
<LazyMotion features={loadFeatures}>
<Router history={history}>
<PageScroll>
<PageTheme />
<ScrollToTop>
<Routes />
</ScrollToTop>
<Toasts />
<Dialogs />
<Desktop />
</PageScroll>
</Router>
</LazyMotion>
</LazyPolyfill>
</KBarProvider>
</ErrorBoundary>
<Router history={history}>
<ErrorBoundary showTitle>
<KBarProvider actions={[]} options={commandBarOptions}>
<LazyPolyfill>
<LazyMotion features={loadFeatures}>
<ActionContextProvider>
<PageScroll>
<PageTheme />
<ScrollToTop>
<Routes />
</ScrollToTop>
<Toasts />
<Dialogs />
<Desktop />
</PageScroll>
</ActionContextProvider>
</LazyMotion>
</LazyPolyfill>
</KBarProvider>
</ErrorBoundary>
</Router>
</Theme>
</Analytics>
</Provider>

View File

@@ -36,7 +36,7 @@ import {
createDocument,
exportCollection,
} from "~/actions/definitions/collections";
import useActionContext from "~/hooks/useActionContext";
import { ActionContextProvider } from "~/hooks/useActionContext";
import usePolicy from "~/hooks/usePolicy";
import useRequest from "~/hooks/useRequest";
import useStores from "~/hooks/useStores";
@@ -130,11 +130,6 @@ function CollectionMenu({
);
const can = usePolicy(collection);
const context = useActionContext({
isContextMenu: true,
activeCollectionId: collection.id,
});
const sortAlphabetical = collection.sort.field === "title";
const sortDir = collection.sort.direction;
@@ -228,7 +223,7 @@ function CollectionMenu({
const rootAction = useMenuAction(actions);
return (
<>
<ActionContextProvider value={{ activeCollectionId: collection.id }}>
<VisuallyHidden.Root>
<label>
{t("Import document")}
@@ -244,7 +239,6 @@ function CollectionMenu({
</VisuallyHidden.Root>
<DropdownMenu
action={rootAction}
context={context}
align={align}
onOpen={onOpen}
onClose={onClose}
@@ -255,7 +249,7 @@ function CollectionMenu({
onPointerEnter={handlePointerEnter}
/>
</DropdownMenu>
</>
</ActionContextProvider>
);
}

View File

@@ -10,7 +10,7 @@ import Document from "~/models/Document";
import { DropdownMenu } from "~/components/Menu/DropdownMenu";
import { OverflowMenuButton } from "~/components/Menu/OverflowMenuButton";
import Switch from "~/components/Switch";
import useActionContext from "~/hooks/useActionContext";
import { ActionContextProvider } from "~/hooks/useActionContext";
import useCurrentUser from "~/hooks/useCurrentUser";
import useMobile from "~/hooks/useMobile";
import usePolicy from "~/hooks/usePolicy";
@@ -132,13 +132,6 @@ function DocumentMenu({
onSelectTemplate,
});
const context = useActionContext({
isContextMenu: true,
activeDocumentId: document.id,
activeCollectionId:
!isShared && document.collectionId ? document.collectionId : undefined,
});
const toggleSwitches = React.useMemo<React.ReactNode>(() => {
if (!can.update || !(showDisplayOptions || showToggleEmbeds)) {
return;
@@ -203,20 +196,29 @@ function DocumentMenu({
]);
return (
<DropdownMenu
action={rootAction}
context={context}
align={align}
onOpen={onOpen}
onClose={onClose}
ariaLabel={t("Document options")}
append={toggleSwitches}
<ActionContextProvider
value={{
activeDocumentId: document.id,
activeCollectionId:
!isShared && document.collectionId
? document.collectionId
: undefined,
}}
>
<OverflowMenuButton
neutral={neutral}
onPointerEnter={handlePointerEnter}
/>
</DropdownMenu>
<DropdownMenu
action={rootAction}
align={align}
onOpen={onOpen}
onClose={onClose}
ariaLabel={t("Document options")}
append={toggleSwitches}
>
<OverflowMenuButton
neutral={neutral}
onPointerEnter={handlePointerEnter}
/>
</DropdownMenu>
</ActionContextProvider>
);
}

View File

@@ -8,9 +8,9 @@ import {
copyLinkToRevision,
restoreRevision,
} from "~/actions/definitions/revisions";
import useActionContext from "~/hooks/useActionContext";
import { useMemo } from "react";
import { useMenuAction } from "~/hooks/useMenuAction";
import { ActionContextProvider } from "~/hooks/useActionContext";
type Props = {
document: Document;
@@ -19,11 +19,6 @@ type Props = {
function RevisionMenu({ document }: Props) {
const { t } = useTranslation();
const context = useActionContext({
isContextMenu: true,
activeDocumentId: document.id,
});
const actions = useMemo(
() => [restoreRevision, ActionV2Separator, copyLinkToRevision],
[]
@@ -32,14 +27,15 @@ function RevisionMenu({ document }: Props) {
const rootAction = useMenuAction(actions);
return (
<DropdownMenu
action={rootAction}
context={context}
align="end"
ariaLabel={t("Revision options")}
>
<OverflowMenuButton />
</DropdownMenu>
<ActionContextProvider value={{ activeDocumentId: document.id }}>
<DropdownMenu
action={rootAction}
align="end"
ariaLabel={t("Revision options")}
>
<OverflowMenuButton />
</DropdownMenu>
</ActionContextProvider>
);
}

View File

@@ -21,7 +21,7 @@ type Props = {
const TeamMenu: React.FC = ({ children }: Props) => {
const { t } = useTranslation();
const context = useActionContext({ isContextMenu: true });
const context = useActionContext({ isMenu: true });
// NOTE: it's useful to memoize on the team id and session because the action
// menu is not cached at all.

View File

@@ -8,7 +8,6 @@ import { AvatarSize } from "~/components/Avatar";
import Facepile from "~/components/Facepile";
import Fade from "~/components/Fade";
import NudeButton from "~/components/NudeButton";
import useActionContext from "~/hooks/useActionContext";
import useMobile from "~/hooks/useMobile";
import useStores from "~/hooks/useStores";
@@ -24,7 +23,6 @@ const MembershipPreview = ({ collection, limit = 8 }: Props) => {
const { t } = useTranslation();
const { memberships, groupMemberships, users } = useStores();
const collectionUsers = users.inCollection(collection.id);
const context = useActionContext();
const isMobile = useMobile();
useEffect(() => {
@@ -72,7 +70,6 @@ const MembershipPreview = ({ collection, limit = 8 }: Props) => {
return (
<NudeButton
context={context}
tooltip={{
content:
usersCount > 0

View File

@@ -24,7 +24,6 @@ import Text from "~/components/Text";
import Time from "~/components/Time";
import Tooltip from "~/components/Tooltip";
import { resolveCommentFactory } from "~/actions/definitions/comments";
import useActionContext from "~/hooks/useActionContext";
import useBoolean from "~/hooks/useBoolean";
import useCurrentUser from "~/hooks/useCurrentUser";
import CommentMenu from "~/menus/CommentMenu";
@@ -312,14 +311,12 @@ const ResolveButton = ({
comment: Comment;
onUpdate: (attrs: { resolved: boolean }) => void;
}) => {
const context = useActionContext();
const { t } = useTranslation();
return (
<Tooltip content={t("Mark as resolved")} placement="top">
<Action
as={NudeButton}
context={context}
action={resolveCommentFactory({
comment,
onResolve: () => onUpdate({ resolved: true }),

View File

@@ -11,12 +11,12 @@ import Revision from "~/models/Revision";
import { openDocumentInsights } from "~/actions/definitions/documents";
import DocumentMeta from "~/components/DocumentMeta";
import Fade from "~/components/Fade";
import useActionContext from "~/hooks/useActionContext";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import { documentPath } from "~/utils/routeHelpers";
import NudeButton from "~/components/NudeButton";
type Props = {
/* The document to display meta data for */
@@ -36,9 +36,6 @@ function TitleDocumentMeta({ to, document, revision, ...rest }: Props) {
const onlyYou = totalViewers === 1 && documentViews[0].userId;
const viewsLoadedOnMount = useRef(totalViewers > 0);
const can = usePolicy(document);
const actionContext = useActionContext({
activeDocumentId: document.id,
});
const Wrapper = viewsLoadedOnMount.current ? Fragment : Fade;
@@ -70,9 +67,7 @@ function TitleDocumentMeta({ to, document, revision, ...rest }: Props) {
!document.isTemplate ? (
<Wrapper>
&nbsp;&nbsp;
<InsightsButton
onClick={() => openDocumentInsights.perform(actionContext)}
>
<InsightsButton action={openDocumentInsights}>
{t("Viewed by")}{" "}
{onlyYou
? t("only you")
@@ -91,7 +86,7 @@ const CommentLink = styled(Link)`
align-items: center;
`;
const InsightsButton = styled.button`
const InsightsButton = styled(NudeButton)`
background: none;
border: none;
padding: 0;

View File

@@ -23,7 +23,6 @@ import Tooltip from "~/components/Tooltip";
import { publishDocument } from "~/actions/definitions/documents";
import { navigateToTemplateSettings } from "~/actions/definitions/navigation";
import { restoreRevision } from "~/actions/definitions/revisions";
import useActionContext from "~/hooks/useActionContext";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useCurrentUser from "~/hooks/useCurrentUser";
import useEditingFocus from "~/hooks/useEditingFocus";
@@ -109,10 +108,6 @@ function DocumentHeader({
}
}, [ui, isShare]);
const context = useActionContext({
activeDocumentId: document?.id,
});
const can = usePolicy(document);
const { isDeleted, isTemplate } = document;
const isTemplateEditable = can.update && isTemplate;
@@ -279,7 +274,6 @@ function DocumentHeader({
placement="bottom"
>
<Button
context={context}
action={isTemplate ? navigateToTemplateSettings : undefined}
onClick={isTemplate ? undefined : handleSave}
disabled={savingIsDisabled}
@@ -308,12 +302,7 @@ function DocumentHeader({
{revision && revision.createdAt !== document.updatedAt && (
<Action>
<Tooltip content={t("Restore version")} placement="bottom">
<Button
action={restoreRevision}
context={context}
neutral
hideOnActionDisabled
>
<Button action={restoreRevision} neutral hideOnActionDisabled>
{t("Restore")}
</Button>
</Tooltip>
@@ -323,7 +312,6 @@ function DocumentHeader({
<Action>
<Button
action={publishDocument}
context={context}
disabled={publishingIsDisabled}
hideOnActionDisabled
hideIcon

View File

@@ -6,12 +6,10 @@ import Empty from "~/components/Empty";
import Heading from "~/components/Heading";
import Scene from "~/components/Scene";
import { navigateToHome } from "~/actions/definitions/navigation";
import useActionContext from "~/hooks/useActionContext";
const Error403 = () => {
const { t } = useTranslation();
const history = useHistory();
const context = useActionContext();
return (
<Scene title={t("No access to this doc")}>
@@ -24,7 +22,7 @@ const Error403 = () => {
{t("Please request access from the document owner.")}
</Empty>
<Flex gap={8}>
<Button action={navigateToHome} context={context} hideIcon>
<Button action={navigateToHome} hideIcon>
{t("Home")}
</Button>
<Button onClick={history.goBack} neutral>

View File

@@ -8,11 +8,9 @@ import {
navigateToHome,
navigateToSearch,
} from "~/actions/definitions/navigation";
import useActionContext from "~/hooks/useActionContext";
const Error404 = () => {
const { t } = useTranslation();
const context = useActionContext();
return (
<Scene title={t("Not found")}>
@@ -25,10 +23,10 @@ const Error404 = () => {
</Trans>
</Empty>
<Flex gap={8}>
<Button action={navigateToHome} context={context} neutral hideIcon>
<Button action={navigateToHome} neutral hideIcon>
{t("Home")}
</Button>
<Button action={navigateToSearch} context={context} neutral>
<Button action={navigateToSearch} neutral>
{t("Search")}
</Button>
</Flex>

View File

@@ -6,11 +6,9 @@ import Empty from "~/components/Empty";
import Heading from "~/components/Heading";
import PageTitle from "~/components/PageTitle";
import { navigateToHome } from "~/actions/definitions/navigation";
import useActionContext from "~/hooks/useActionContext";
const ErrorUnknown = () => {
const { t } = useTranslation();
const context = useActionContext();
return (
<CenteredContent>
@@ -24,7 +22,7 @@ const ErrorUnknown = () => {
</Trans>
</Empty>
<Flex gap={8}>
<Button action={navigateToHome} context={context} neutral hideIcon>
<Button action={navigateToHome} neutral hideIcon>
{t("Home")}
</Button>
</Flex>

View File

@@ -11,7 +11,6 @@ import Scene from "~/components/Scene";
import Text from "~/components/Text";
import { createApiKey } from "~/actions/definitions/apiKeys";
import env from "~/env";
import useActionContext from "~/hooks/useActionContext";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useCurrentUser from "~/hooks/useCurrentUser";
import usePolicy from "~/hooks/usePolicy";
@@ -25,7 +24,6 @@ function APIAndApps() {
const { t } = useTranslation();
const { apiKeys, oauthAuthentications } = useStores();
const can = usePolicy(team);
const context = useActionContext();
const appName = env.APP_NAME;
return (
@@ -40,7 +38,6 @@ function APIAndApps() {
type="submit"
value={`${t("New API key")}`}
action={createApiKey}
context={context}
/>
</Action>
)}

View File

@@ -9,7 +9,6 @@ import PaginatedList from "~/components/PaginatedList";
import Scene from "~/components/Scene";
import Text from "~/components/Text";
import { createApiKey } from "~/actions/definitions/apiKeys";
import useActionContext from "~/hooks/useActionContext";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
@@ -20,7 +19,6 @@ function ApiKeys() {
const { t } = useTranslation();
const { apiKeys } = useStores();
const can = usePolicy(team);
const context = useActionContext();
return (
<Scene
@@ -34,7 +32,6 @@ function ApiKeys() {
type="submit"
value={`${t("New API key")}`}
action={createApiKey}
context={context}
/>
</Action>
)}

View File

@@ -9,7 +9,6 @@ import PaginatedList from "~/components/PaginatedList";
import Scene from "~/components/Scene";
import Text from "~/components/Text";
import { createOAuthClient } from "~/actions/definitions/oauthClients";
import useActionContext from "~/hooks/useActionContext";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
@@ -20,7 +19,6 @@ function Applications() {
const { t } = useTranslation();
const { oauthClients } = useStores();
const can = usePolicy(team);
const context = useActionContext();
return (
<Scene
@@ -34,7 +32,6 @@ function Applications() {
type="submit"
value={`${t("New App")}`}
action={createOAuthClient}
context={context}
/>
</Action>
)}

View File

@@ -16,7 +16,6 @@ import Scene from "~/components/Scene";
import Text from "~/components/Text";
import { inviteUser } from "~/actions/definitions/users";
import env from "~/env";
import useActionContext from "~/hooks/useActionContext";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import usePolicy from "~/hooks/usePolicy";
import useQuery from "~/hooks/useQuery";
@@ -32,7 +31,6 @@ function Members() {
const location = useLocation();
const history = useHistory();
const team = useCurrentTeam();
const context = useActionContext();
const { users } = useStores();
const { t } = useTranslation();
const params = useQuery();
@@ -128,7 +126,6 @@ function Members() {
data-event-category="invite"
data-event-action="peoplePage"
action={inviteUser}
context={context}
icon={<PlusIcon />}
>
{t("Invite people")}

View File

@@ -8,24 +8,19 @@ import PaginatedDocumentList from "~/components/PaginatedDocumentList";
import Scene from "~/components/Scene";
import Subheading from "~/components/Subheading";
import { permanentlyDeleteDocumentsInTrash } from "~/actions/definitions/documents";
import useActionContext from "~/hooks/useActionContext";
import useStores from "~/hooks/useStores";
function Trash() {
const { t } = useTranslation();
const { documents } = useStores();
const context = useActionContext();
return (
<Scene
icon={<TrashIcon />}
title={t("Trash")}
actions={
documents.deleted.length > 0 && (
<Button
neutral
action={permanentlyDeleteDocumentsInTrash}
context={context}
>
<Button neutral action={permanentlyDeleteDocumentsInTrash}>
{t("Empty trash")}
</Button>
)

View File

@@ -92,7 +92,7 @@ export type MenuItem =
| MenuGroup;
export type ActionContext = {
isContextMenu: boolean;
isMenu: boolean;
isCommandBar: boolean;
isButton: boolean;
sidebarContext?: SidebarContextType;

View File

@@ -15,7 +15,6 @@ import Input from "~/components/Input";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
import { disconnectAnalyticsIntegrationFactory } from "~/actions/definitions/integrations";
import useActionContext from "~/hooks/useActionContext";
import Flex from "~/components/Flex";
import styled from "styled-components";
@@ -26,7 +25,6 @@ type FormData = {
function GoogleAnalytics() {
const { integrations } = useStores();
const { t } = useTranslation();
const context = useActionContext();
const integration = find(integrations.orderedData, {
type: IntegrationType.Analytics,
@@ -108,7 +106,6 @@ function GoogleAnalytics() {
<Button
action={disconnectAnalyticsIntegrationFactory(integration)}
context={context}
disabled={formState.isSubmitting}
neutral
hideIcon

View File

@@ -15,7 +15,6 @@ import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
import Icon from "./Icon";
import { disconnectAnalyticsIntegrationFactory } from "~/actions/definitions/integrations";
import useActionContext from "~/hooks/useActionContext";
import Flex from "~/components/Flex";
import styled from "styled-components";
@@ -27,7 +26,6 @@ type FormData = {
function Matomo() {
const { integrations } = useStores();
const { t } = useTranslation();
const context = useActionContext();
const integration = find(integrations.orderedData, {
type: IntegrationType.Analytics,
@@ -129,7 +127,6 @@ function Matomo() {
<Button
action={disconnectAnalyticsIntegrationFactory(integration)}
context={context}
disabled={formState.isSubmitting}
neutral
hideIcon

View File

@@ -16,7 +16,6 @@ import useStores from "~/hooks/useStores";
import Icon from "./Icon";
import Flex from "~/components/Flex";
import { disconnectAnalyticsIntegrationFactory } from "~/actions/definitions/integrations";
import useActionContext from "~/hooks/useActionContext";
import styled from "styled-components";
type FormData = {
@@ -28,7 +27,6 @@ type FormData = {
function Umami() {
const { integrations } = useStores();
const { t } = useTranslation();
const context = useActionContext();
const integration = find(integrations.orderedData, {
type: IntegrationType.Analytics,
@@ -149,7 +147,6 @@ function Umami() {
<Button
action={disconnectAnalyticsIntegrationFactory(integration)}
context={context}
disabled={formState.isSubmitting}
neutral
hideIcon