Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
pedrobonamin marked this conversation as resolved.
Show resolved Hide resolved
}

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
Loading