diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 393f657e313e..96f99010b424 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -73,7 +73,17 @@ const config = { { ignores: { componentPatterns: ['motion$'], - attributes: ['animate', 'closed', 'exit', 'fill', 'full', 'initial', 'size', 'sortOrder'], + attributes: [ + 'animate', + 'closed', + 'exit', + 'fill', + 'full', + 'initial', + 'size', + 'sortOrder', + 'group', + ], }, }, ], diff --git a/packages/sanity/src/core/components/hookCollection/GetHookCollectionState.tsx b/packages/sanity/src/core/components/hookCollection/GetHookCollectionState.tsx index 9e4ae4c8ef59..d15eed5c4737 100644 --- a/packages/sanity/src/core/components/hookCollection/GetHookCollectionState.tsx +++ b/packages/sanity/src/core/components/hookCollection/GetHookCollectionState.tsx @@ -9,17 +9,24 @@ import {type ActionHook} from './types' /** @internal */ export interface GetHookCollectionStateProps { + /** + * Arguments that will be received by the action hooks, `onComplete` will be added by the HookStateContainer component. + */ args: T children: (props: {states: K[]}) => ReactNode - hooks: ActionHook[] + hooks: ActionHook void}, K>[] onReset?: () => void + /** + * Name for the hook group. If provided, only hooks with the same group name will be included in the collection. + */ + group?: string } const throttleOptions: ThrottleSettings = {trailing: true} /** @internal */ export function GetHookCollectionState(props: GetHookCollectionStateProps) { - const {hooks, args, children, onReset} = props + const {hooks, args, children, group, onReset} = props const statesRef = useRef>({}) const [tickId, setTick] = useState(0) @@ -46,14 +53,18 @@ export function GetHookCollectionState(props: GetHookCollectionStateProps< throttleOptions, ) - const handleNext = useCallback((id: any, hookState: any) => { - if (hookState === null) { - delete statesRef.current[id] - } else { - const current = statesRef.current[id] - statesRef.current[id] = {...current, value: hookState} - } - }, []) + const handleNext = useCallback( + (id: any, hookState: any) => { + const hookGroup = hookState?.group || ['default'] + if (hookState === null || (group && !hookGroup.includes(group))) { + delete statesRef.current[id] + } else { + const current = statesRef.current[id] + statesRef.current[id] = {...current, value: hookState} + } + }, + [group], + ) const handleReset = useCallback( (id: any) => { @@ -67,7 +78,6 @@ export function GetHookCollectionState(props: GetHookCollectionStateProps< ) const hookIds = useMemo(() => hooks.map((hook) => getHookId(hook)), [hooks]) - const states = useMemo( () => hookIds.map((id) => statesRef.current[id]?.value).filter(isNonNullable), // eslint-disable-next-line react-hooks/exhaustive-deps -- tickId is used to refresh the memo, before it can be removed it needs to be investigated what impact it has diff --git a/packages/sanity/src/core/config/document/actions.ts b/packages/sanity/src/core/config/document/actions.ts index 99d22a57af01..865020663d3a 100644 --- a/packages/sanity/src/core/config/document/actions.ts +++ b/packages/sanity/src/core/config/document/actions.ts @@ -115,6 +115,11 @@ export type DocumentActionDialogProps = | DocumentActionModalDialogProps | DocumentActionCustomDialogComponentProps +/** + * @hidden + * @beta */ +export type DocumentActionGroup = 'default' | 'paneActions' + /** * @hidden * @beta */ @@ -127,4 +132,8 @@ export interface DocumentActionDescription { onHandle?: () => void shortcut?: string | null title?: ReactNode + /** + * @beta + */ + group?: DocumentActionGroup[] } diff --git a/packages/sanity/src/structure/components/RenderActionCollectionState.tsx b/packages/sanity/src/structure/components/RenderActionCollectionState.tsx index a5bb89fbcad4..94b540094d31 100644 --- a/packages/sanity/src/structure/components/RenderActionCollectionState.tsx +++ b/packages/sanity/src/structure/components/RenderActionCollectionState.tsx @@ -1,6 +1,7 @@ import type * as React from 'react' import { type DocumentActionDescription, + type DocumentActionGroup, type DocumentActionProps, GetHookCollectionState, } from 'sanity' @@ -13,17 +14,23 @@ export interface Action { /** @internal */ export interface RenderActionCollectionProps { actions: Action[] - actionProps: DocumentActionProps + actionProps: Omit children: (props: {states: DocumentActionDescription[]}) => React.ReactNode onActionComplete?: () => void + group?: DocumentActionGroup } /** @internal */ export const RenderActionCollectionState = (props: RenderActionCollectionProps) => { - const {actions, children, actionProps, onActionComplete} = props + const {actions, children, actionProps, onActionComplete, group} = props return ( - + {children} ) diff --git a/packages/sanity/src/structure/components/pane/PaneContextMenuButton.tsx b/packages/sanity/src/structure/components/pane/PaneContextMenuButton.tsx index f0d16e7e908c..2d0e966abd42 100644 --- a/packages/sanity/src/structure/components/pane/PaneContextMenuButton.tsx +++ b/packages/sanity/src/structure/components/pane/PaneContextMenuButton.tsx @@ -1,5 +1,5 @@ -import {Menu} from '@sanity/ui' -import {useId} from 'react' +import {Menu, MenuDivider} from '@sanity/ui' +import {type ReactNode, useId} from 'react' import {ContextMenuButton} from 'sanity' import {MenuButton, type PopoverProps} from '../../../ui-components' @@ -8,6 +8,7 @@ import {type _PaneMenuItem, type _PaneMenuNode} from './types' interface PaneContextMenuButtonProps { nodes: _PaneMenuNode[] + actionsNodes?: ReactNode } const CONTEXT_MENU_POPOVER_PROPS: PopoverProps = { @@ -31,7 +32,7 @@ function nodesHasTone(nodes: _PaneMenuNode[], tone: NonNullable<_PaneMenuItem['t * @beta This API will change. DO NOT USE IN PRODUCTION. */ export function PaneContextMenuButton(props: PaneContextMenuButtonProps) { - const {nodes} = props + const {nodes, actionsNodes} = props const id = useId() const hasCritical = nodesHasTone(nodes, 'critical') @@ -49,9 +50,14 @@ export function PaneContextMenuButton(props: PaneContextMenuButtonProps) { id={id} menu={ + {actionsNodes && ( + <> + {actionsNodes} + + + )} {nodes.map((node, nodeIndex) => { const isAfterGroup = nodes[nodeIndex - 1]?.type === 'group' - return })} diff --git a/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentPanelHeader.tsx b/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentPanelHeader.tsx index 8a0e3995ba80..595e209b87f5 100644 --- a/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentPanelHeader.tsx +++ b/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentPanelHeader.tsx @@ -1,7 +1,7 @@ import {ArrowLeftIcon, CloseIcon, SplitVerticalIcon} from '@sanity/icons' import {Flex} from '@sanity/ui' import type * as React from 'react' -import {createElement, forwardRef, memo, useMemo} from 'react' +import {createElement, forwardRef, memo, useMemo, useState} from 'react' import {useFieldActions, useTimelineSelector, useTranslation} from 'sanity' import {Button, TooltipDelayGroupProvider} from '../../../../../ui-components' @@ -9,6 +9,7 @@ import { PaneContextMenuButton, PaneHeader, PaneHeaderActionButton, + RenderActionCollectionState, usePane, usePaneRouter, } from '../../../../components' @@ -16,6 +17,7 @@ import {structureLocaleNamespace} from '../../../../i18n' import {isMenuNodeButton, isNotMenuNodeButton, resolveMenuNodes} from '../../../../menuNodes' import {type PaneMenuItem} from '../../../../types' import {useStructureTool} from '../../../../useStructureTool' +import {ActionDialogWrapper, ActionMenuListItem} from '../../statusBar/ActionMenuButton' import {TimelineMenu} from '../../timeline' import {useDocumentPane} from '../../useDocumentPane' import {DocumentHeaderTabs} from './DocumentHeaderTabs' @@ -33,6 +35,8 @@ export const DocumentPanelHeader = memo( ) { const {menuItems} = _props const { + actions, + editState, onMenuAction, onPaneClose, onPaneSplit, @@ -46,6 +50,7 @@ export const DocumentPanelHeader = memo( const {features} = useStructureTool() const {index, BackLink, hasGroupSiblings} = usePaneRouter() const {actions: fieldActions} = useFieldActions() + const [referenceElement, setReferenceElement] = useState(null) const menuNodes = useMemo( () => @@ -129,8 +134,35 @@ export const DocumentPanelHeader = memo( {menuButtonNodes.map((item) => ( ))} - - + {editState && ( + + {({states}) => ( + + {({handleAction}) => ( +
+ ( + + ))} + /> +
+ )} +
+ )} +
+ )} {showSplitPaneButton && (