Skip to content

Commit

Permalink
feat(core): add groups to document actions, introduce paneActions group
Browse files Browse the repository at this point in the history
  • Loading branch information
pedrobonamin committed Mar 8, 2024
1 parent 3670459 commit df45f7d
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 62 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const config = {
{
ignores: {
componentPatterns: ['motion$'],
attributes: ['animate', 'closed', 'exit', 'fill', 'full', 'initial', 'size'],
attributes: ['animate', 'closed', 'exit', 'fill', 'full', 'initial', 'size', 'group'],
},
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ export interface GetHookCollectionStateProps<T, K> {
children: (props: {states: K[]}) => ReactNode
hooks: ActionHook<T, K>[]
onReset?: () => void
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 +47,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 +72,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 @@ -16,14 +17,20 @@ export interface RenderActionCollectionProps {
actionProps: DocumentActionProps
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 @@ -130,8 +135,34 @@ export const DocumentPanelHeader = memo(
<PaneHeaderActionButton key={item.key} node={item} />
))}

<PaneContextMenuButton nodes={contextMenuNodes} key="context-menu" />

<RenderActionCollectionState
actions={actions || []}
// @ts-expect-error TODO: fix the document 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
aria-label={t('buttons.split-pane-button.aria-label')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,46 @@ export interface ActionMenuButtonProps {
disabled: boolean
}

export function ActionMenuButton(props: ActionMenuButtonProps) {
const {actionStates, disabled} = props
const idPrefix = useId()
/**
* @internal
*/
export function ActionDialogWrapper({
actionStates,
children,
referenceElement,
}: {
actionStates: DocumentActionDescription[]
children: ({handleAction}: {handleAction: (idx: number) => void}) => React.ReactNode
referenceElement?: HTMLElement | null
}) {
const [actionIndex, setActionIndex] = useState(-1)
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null)
const currentAction = actionStates[actionIndex]

const handleAction = useCallback((idx: number) => {
setActionIndex(idx)
}, [])

return (
<>
{currentAction && currentAction.dialog && (
<LegacyLayerProvider zOffset="paneFooter">
<ActionStateDialog dialog={currentAction.dialog} referenceElement={referenceElement} />
</LegacyLayerProvider>
)}
{children({handleAction})}
</>
)
}

/**
* @internal
*/
export function ActionMenuButton(props: ActionMenuButtonProps) {
const {actionStates, disabled} = props
const idPrefix = useId()

const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null)

const popoverProps: PopoverProps = useMemo(
() => ({
placement: 'top-end',
Expand All @@ -35,46 +65,41 @@ export function ActionMenuButton(props: ActionMenuButtonProps) {
[],
)

const currentAction = actionStates[actionIndex]
const {t} = useTranslation(structureLocaleNamespace)

return (
<>
<MenuButton
id={`${idPrefix}-action-menu`}
button={
<ContextMenuButton
aria-label={t('buttons.action-menu-button.aria-label')}
disabled={disabled}
data-testid="action-menu-button"
size="large"
tooltipProps={{content: t('buttons.action-menu-button.tooltip')}}
/>
}
menu={
<Menu padding={1}>
{actionStates.map((actionState, idx) => (
<ActionMenuListItem
actionState={actionState}
disabled={disabled}
index={idx}
// eslint-disable-next-line react/no-array-index-key
key={idx}
onAction={handleAction}
/>
))}
</Menu>
}
popover={popoverProps}
ref={setReferenceElement}
/>

{currentAction && currentAction.dialog && (
<LegacyLayerProvider zOffset="paneFooter">
<ActionStateDialog dialog={currentAction.dialog} referenceElement={referenceElement} />
</LegacyLayerProvider>
<ActionDialogWrapper actionStates={actionStates} referenceElement={referenceElement}>
{({handleAction}) => (
<MenuButton
id={`${idPrefix}-action-menu`}
button={
<ContextMenuButton
aria-label={t('buttons.action-menu-button.aria-label')}
disabled={disabled}
data-testid="action-menu-button"
size="large"
tooltipProps={{content: t('buttons.action-menu-button.tooltip')}}
/>
}
menu={
<Menu padding={1}>
{actionStates.map((actionState, idx) => (
<ActionMenuListItem
actionState={actionState}
disabled={disabled}
index={idx}
// eslint-disable-next-line react/no-array-index-key
key={idx}
onAction={handleAction}
/>
))}
</Menu>
}
popover={popoverProps}
ref={setReferenceElement}
/>
)}
</>
</ActionDialogWrapper>
)
}

Expand All @@ -85,7 +110,7 @@ interface ActionMenuListItemProps {
onAction: (idx: number) => void
}

function ActionMenuListItem(props: ActionMenuListItemProps) {
export function ActionMenuListItem(props: ActionMenuListItemProps) {
const {actionState, disabled, index, onAction} = props
const {onHandle} = actionState

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export const DocumentStatusBarActions = memo(function DocumentStatusBarActions()
actions={actions}
// @ts-expect-error TODO: fix the document actions
actionProps={editState}
group="default"
>
{({states}) => (
<DocumentStatusBarActionsInner
Expand Down Expand Up @@ -118,7 +119,11 @@ export const HistoryStatusBarActions = memo(function HistoryStatusBarActions() {
const historyActions = useMemo(() => [HistoryRestoreAction], [])

return (
<RenderActionCollectionState actions={historyActions} actionProps={actionProps as any}>
<RenderActionCollectionState
actions={historyActions}
actionProps={actionProps as any}
group="default"
>
{({states}) => (
<DocumentStatusBarActionsInner
disabled={connectionState !== 'connected' || Boolean(disabled)}
Expand Down

0 comments on commit df45f7d

Please sign in to comment.