Skip to content

Commit 4e95e09

Browse files
authoredMar 11, 2024
feat(core): add groups to document actions, introduce paneActions group (#5933)
* feat(core): add groups to document actions, introduce paneActions group * fix(core): update GetHookCollectionState types
1 parent 01f7df2 commit 4e95e09

File tree

8 files changed

+169
-66
lines changed

8 files changed

+169
-66
lines changed
 

‎.eslintrc.cjs

+11-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,17 @@ const config = {
7373
{
7474
ignores: {
7575
componentPatterns: ['motion$'],
76-
attributes: ['animate', 'closed', 'exit', 'fill', 'full', 'initial', 'size', 'sortOrder'],
76+
attributes: [
77+
'animate',
78+
'closed',
79+
'exit',
80+
'fill',
81+
'full',
82+
'initial',
83+
'size',
84+
'sortOrder',
85+
'group',
86+
],
7787
},
7888
},
7989
],

‎packages/sanity/src/core/components/hookCollection/GetHookCollectionState.tsx

+21-11
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,24 @@ import {type ActionHook} from './types'
99

1010
/** @internal */
1111
export interface GetHookCollectionStateProps<T, K> {
12+
/**
13+
* Arguments that will be received by the action hooks, `onComplete` will be added by the HookStateContainer component.
14+
*/
1215
args: T
1316
children: (props: {states: K[]}) => ReactNode
14-
hooks: ActionHook<T, K>[]
17+
hooks: ActionHook<T & {onComplete: () => void}, K>[]
1518
onReset?: () => void
19+
/**
20+
* Name for the hook group. If provided, only hooks with the same group name will be included in the collection.
21+
*/
22+
group?: string
1623
}
1724

1825
const throttleOptions: ThrottleSettings = {trailing: true}
1926

2027
/** @internal */
2128
export function GetHookCollectionState<T, K>(props: GetHookCollectionStateProps<T, K>) {
22-
const {hooks, args, children, onReset} = props
29+
const {hooks, args, children, group, onReset} = props
2330

2431
const statesRef = useRef<Record<string, {value: K}>>({})
2532
const [tickId, setTick] = useState(0)
@@ -46,14 +53,18 @@ export function GetHookCollectionState<T, K>(props: GetHookCollectionStateProps<
4653
throttleOptions,
4754
)
4855

49-
const handleNext = useCallback((id: any, hookState: any) => {
50-
if (hookState === null) {
51-
delete statesRef.current[id]
52-
} else {
53-
const current = statesRef.current[id]
54-
statesRef.current[id] = {...current, value: hookState}
55-
}
56-
}, [])
56+
const handleNext = useCallback(
57+
(id: any, hookState: any) => {
58+
const hookGroup = hookState?.group || ['default']
59+
if (hookState === null || (group && !hookGroup.includes(group))) {
60+
delete statesRef.current[id]
61+
} else {
62+
const current = statesRef.current[id]
63+
statesRef.current[id] = {...current, value: hookState}
64+
}
65+
},
66+
[group],
67+
)
5768

5869
const handleReset = useCallback(
5970
(id: any) => {
@@ -67,7 +78,6 @@ export function GetHookCollectionState<T, K>(props: GetHookCollectionStateProps<
6778
)
6879

6980
const hookIds = useMemo(() => hooks.map((hook) => getHookId(hook)), [hooks])
70-
7181
const states = useMemo(
7282
() => hookIds.map((id) => statesRef.current[id]?.value).filter(isNonNullable),
7383
// 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

‎packages/sanity/src/core/config/document/actions.ts

+9
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ export type DocumentActionDialogProps =
115115
| DocumentActionModalDialogProps
116116
| DocumentActionCustomDialogComponentProps
117117

118+
/**
119+
* @hidden
120+
* @beta */
121+
export type DocumentActionGroup = 'default' | 'paneActions'
122+
118123
/**
119124
* @hidden
120125
* @beta */
@@ -127,4 +132,8 @@ export interface DocumentActionDescription {
127132
onHandle?: () => void
128133
shortcut?: string | null
129134
title?: ReactNode
135+
/**
136+
* @beta
137+
*/
138+
group?: DocumentActionGroup[]
130139
}

‎packages/sanity/src/structure/components/RenderActionCollectionState.tsx

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type * as React from 'react'
22
import {
33
type DocumentActionDescription,
4+
type DocumentActionGroup,
45
type DocumentActionProps,
56
GetHookCollectionState,
67
} from 'sanity'
@@ -13,17 +14,23 @@ export interface Action<Args, Description> {
1314
/** @internal */
1415
export interface RenderActionCollectionProps {
1516
actions: Action<DocumentActionProps, DocumentActionDescription>[]
16-
actionProps: DocumentActionProps
17+
actionProps: Omit<DocumentActionProps, 'onComplete'>
1718
children: (props: {states: DocumentActionDescription[]}) => React.ReactNode
1819
onActionComplete?: () => void
20+
group?: DocumentActionGroup
1921
}
2022

2123
/** @internal */
2224
export const RenderActionCollectionState = (props: RenderActionCollectionProps) => {
23-
const {actions, children, actionProps, onActionComplete} = props
25+
const {actions, children, actionProps, onActionComplete, group} = props
2426

2527
return (
26-
<GetHookCollectionState onReset={onActionComplete} hooks={actions} args={actionProps}>
28+
<GetHookCollectionState
29+
onReset={onActionComplete}
30+
hooks={actions}
31+
args={actionProps}
32+
group={group}
33+
>
2734
{children}
2835
</GetHookCollectionState>
2936
)

‎packages/sanity/src/structure/components/pane/PaneContextMenuButton.tsx

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import {Menu} from '@sanity/ui'
2-
import {useId} from 'react'
1+
import {Menu, MenuDivider} from '@sanity/ui'
2+
import {type ReactNode, useId} from 'react'
33
import {ContextMenuButton} from 'sanity'
44

55
import {MenuButton, type PopoverProps} from '../../../ui-components'
@@ -8,6 +8,7 @@ import {type _PaneMenuItem, type _PaneMenuNode} from './types'
88

99
interface PaneContextMenuButtonProps {
1010
nodes: _PaneMenuNode[]
11+
actionsNodes?: ReactNode
1112
}
1213

1314
const CONTEXT_MENU_POPOVER_PROPS: PopoverProps = {
@@ -31,7 +32,7 @@ function nodesHasTone(nodes: _PaneMenuNode[], tone: NonNullable<_PaneMenuItem['t
3132
* @beta This API will change. DO NOT USE IN PRODUCTION.
3233
*/
3334
export function PaneContextMenuButton(props: PaneContextMenuButtonProps) {
34-
const {nodes} = props
35+
const {nodes, actionsNodes} = props
3536
const id = useId()
3637

3738
const hasCritical = nodesHasTone(nodes, 'critical')
@@ -49,9 +50,14 @@ export function PaneContextMenuButton(props: PaneContextMenuButtonProps) {
4950
id={id}
5051
menu={
5152
<Menu>
53+
{actionsNodes && (
54+
<>
55+
{actionsNodes}
56+
<MenuDivider />
57+
</>
58+
)}
5259
{nodes.map((node, nodeIndex) => {
5360
const isAfterGroup = nodes[nodeIndex - 1]?.type === 'group'
54-
5561
return <PaneMenuButtonItem isAfterGroup={isAfterGroup} key={node.key} node={node} />
5662
})}
5763
</Menu>

‎packages/sanity/src/structure/panes/document/documentPanel/header/DocumentPanelHeader.tsx

+35-3
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import {ArrowLeftIcon, CloseIcon, SplitVerticalIcon} from '@sanity/icons'
22
import {Flex} from '@sanity/ui'
33
import type * as React from 'react'
4-
import {createElement, forwardRef, memo, useMemo} from 'react'
4+
import {createElement, forwardRef, memo, useMemo, useState} from 'react'
55
import {useFieldActions, useTimelineSelector, useTranslation} from 'sanity'
66

77
import {Button, TooltipDelayGroupProvider} from '../../../../../ui-components'
88
import {
99
PaneContextMenuButton,
1010
PaneHeader,
1111
PaneHeaderActionButton,
12+
RenderActionCollectionState,
1213
usePane,
1314
usePaneRouter,
1415
} from '../../../../components'
1516
import {structureLocaleNamespace} from '../../../../i18n'
1617
import {isMenuNodeButton, isNotMenuNodeButton, resolveMenuNodes} from '../../../../menuNodes'
1718
import {type PaneMenuItem} from '../../../../types'
1819
import {useStructureTool} from '../../../../useStructureTool'
20+
import {ActionDialogWrapper, ActionMenuListItem} from '../../statusBar/ActionMenuButton'
1921
import {TimelineMenu} from '../../timeline'
2022
import {useDocumentPane} from '../../useDocumentPane'
2123
import {DocumentHeaderTabs} from './DocumentHeaderTabs'
@@ -33,6 +35,8 @@ export const DocumentPanelHeader = memo(
3335
) {
3436
const {menuItems} = _props
3537
const {
38+
actions,
39+
editState,
3640
onMenuAction,
3741
onPaneClose,
3842
onPaneSplit,
@@ -46,6 +50,7 @@ export const DocumentPanelHeader = memo(
4650
const {features} = useStructureTool()
4751
const {index, BackLink, hasGroupSiblings} = usePaneRouter()
4852
const {actions: fieldActions} = useFieldActions()
53+
const [referenceElement, setReferenceElement] = useState<HTMLElement | null>(null)
4954

5055
const menuNodes = useMemo(
5156
() =>
@@ -129,8 +134,35 @@ export const DocumentPanelHeader = memo(
129134
{menuButtonNodes.map((item) => (
130135
<PaneHeaderActionButton key={item.key} node={item} />
131136
))}
132-
133-
<PaneContextMenuButton nodes={contextMenuNodes} key="context-menu" />
137+
{editState && (
138+
<RenderActionCollectionState
139+
actions={actions || []}
140+
actionProps={editState}
141+
group="paneActions"
142+
>
143+
{({states}) => (
144+
<ActionDialogWrapper actionStates={states} referenceElement={referenceElement}>
145+
{({handleAction}) => (
146+
<div ref={setReferenceElement}>
147+
<PaneContextMenuButton
148+
nodes={contextMenuNodes}
149+
key="context-menu"
150+
actionsNodes={states?.map((actionState, actionIndex) => (
151+
<ActionMenuListItem
152+
key={actionState.label}
153+
actionState={actionState}
154+
disabled={Boolean(actionState.disabled)}
155+
index={actionIndex}
156+
onAction={handleAction}
157+
/>
158+
))}
159+
/>
160+
</div>
161+
)}
162+
</ActionDialogWrapper>
163+
)}
164+
</RenderActionCollectionState>
165+
)}
134166

135167
{showSplitPaneButton && (
136168
<Button

0 commit comments

Comments
 (0)
Please sign in to comment.