Skip to content

Commit

Permalink
feat(core): add groups to document actions, introduce paneActions gro…
Browse files Browse the repository at this point in the history
…up (#5933)

* feat(core): add groups to document actions, introduce paneActions group

* fix(core): update GetHookCollectionState types
  • Loading branch information
pedrobonamin authored Mar 11, 2024
1 parent 01f7df2 commit 4e95e09
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 66 deletions.
12 changes: 11 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],
},
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,24 @@ import {type ActionHook} from './types'

/** @internal */
export interface GetHookCollectionStateProps<T, K> {
/**
* 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<T, K>[]
hooks: ActionHook<T & {onComplete: () => 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<T, K>(props: GetHookCollectionStateProps<T, K>) {
const {hooks, args, children, onReset} = props
const {hooks, args, children, group, onReset} = props

const statesRef = useRef<Record<string, {value: K}>>({})
const [tickId, setTick] = useState(0)
Expand All @@ -46,14 +53,18 @@ export function GetHookCollectionState<T, K>(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) => {
Expand All @@ -67,7 +78,6 @@ export function GetHookCollectionState<T, K>(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
Expand Down
9 changes: 9 additions & 0 deletions packages/sanity/src/core/config/document/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ export type DocumentActionDialogProps =
| DocumentActionModalDialogProps
| DocumentActionCustomDialogComponentProps

/**
* @hidden
* @beta */
export type DocumentActionGroup = 'default' | 'paneActions'

/**
* @hidden
* @beta */
Expand All @@ -127,4 +132,8 @@ export interface DocumentActionDescription {
onHandle?: () => void
shortcut?: string | null
title?: ReactNode
/**
* @beta
*/
group?: DocumentActionGroup[]
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type * as React from 'react'
import {
type DocumentActionDescription,
type DocumentActionGroup,
type DocumentActionProps,
GetHookCollectionState,
} from 'sanity'
Expand All @@ -13,17 +14,23 @@ export interface Action<Args, Description> {
/** @internal */
export interface RenderActionCollectionProps {
actions: Action<DocumentActionProps, DocumentActionDescription>[]
actionProps: DocumentActionProps
actionProps: Omit<DocumentActionProps, 'onComplete'>
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 (
<GetHookCollectionState onReset={onActionComplete} hooks={actions} args={actionProps}>
<GetHookCollectionState
onReset={onActionComplete}
hooks={actions}
args={actionProps}
group={group}
>
{children}
</GetHookCollectionState>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -8,6 +8,7 @@ import {type _PaneMenuItem, type _PaneMenuNode} from './types'

interface PaneContextMenuButtonProps {
nodes: _PaneMenuNode[]
actionsNodes?: ReactNode
}

const CONTEXT_MENU_POPOVER_PROPS: PopoverProps = {
Expand All @@ -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')
Expand All @@ -49,9 +50,14 @@ export function PaneContextMenuButton(props: PaneContextMenuButtonProps) {
id={id}
menu={
<Menu>
{actionsNodes && (
<>
{actionsNodes}
<MenuDivider />
</>
)}
{nodes.map((node, nodeIndex) => {
const isAfterGroup = nodes[nodeIndex - 1]?.type === 'group'

return <PaneMenuButtonItem isAfterGroup={isAfterGroup} key={node.key} node={node} />
})}
</Menu>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
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'
import {
PaneContextMenuButton,
PaneHeader,
PaneHeaderActionButton,
RenderActionCollectionState,
usePane,
usePaneRouter,
} from '../../../../components'
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'
Expand All @@ -33,6 +35,8 @@ export const DocumentPanelHeader = memo(
) {
const {menuItems} = _props
const {
actions,
editState,
onMenuAction,
onPaneClose,
onPaneSplit,
Expand All @@ -46,6 +50,7 @@ export const DocumentPanelHeader = memo(
const {features} = useStructureTool()
const {index, BackLink, hasGroupSiblings} = usePaneRouter()
const {actions: fieldActions} = useFieldActions()
const [referenceElement, setReferenceElement] = useState<HTMLElement | null>(null)

const menuNodes = useMemo(
() =>
Expand Down Expand Up @@ -129,8 +134,35 @@ export const DocumentPanelHeader = memo(
{menuButtonNodes.map((item) => (
<PaneHeaderActionButton key={item.key} node={item} />
))}

<PaneContextMenuButton nodes={contextMenuNodes} key="context-menu" />
{editState && (
<RenderActionCollectionState
actions={actions || []}
actionProps={editState}
group="paneActions"
>
{({states}) => (
<ActionDialogWrapper actionStates={states} referenceElement={referenceElement}>
{({handleAction}) => (
<div ref={setReferenceElement}>
<PaneContextMenuButton
nodes={contextMenuNodes}
key="context-menu"
actionsNodes={states?.map((actionState, actionIndex) => (
<ActionMenuListItem
key={actionState.label}
actionState={actionState}
disabled={Boolean(actionState.disabled)}
index={actionIndex}
onAction={handleAction}
/>
))}
/>
</div>
)}
</ActionDialogWrapper>
)}
</RenderActionCollectionState>
)}

{showSplitPaneButton && (
<Button
Expand Down
Loading

0 comments on commit 4e95e09

Please sign in to comment.