diff --git a/dev/studio-e2e-testing/components-api/DocumentLayout.tsx b/dev/studio-e2e-testing/components-api/DocumentLayout.tsx
new file mode 100644
index 00000000000..b79b69b6780
--- /dev/null
+++ b/dev/studio-e2e-testing/components-api/DocumentLayout.tsx
@@ -0,0 +1,16 @@
+import {Flex} from '@sanity/ui'
+import {DocumentLayoutProps} from 'sanity'
+
+export function DocumentLayout(props: DocumentLayoutProps & {testId: string}) {
+ const {testId} = props
+
+ if (props.documentType !== 'formComponentsApi') {
+ return props.renderDefault(props)
+ }
+
+ return (
+
+ {props.renderDefault(props)}
+
+ )
+}
diff --git a/dev/studio-e2e-testing/components-api/FormField.tsx b/dev/studio-e2e-testing/components-api/FormField.tsx
new file mode 100644
index 00000000000..8f3457eab96
--- /dev/null
+++ b/dev/studio-e2e-testing/components-api/FormField.tsx
@@ -0,0 +1,8 @@
+import {Stack} from '@sanity/ui'
+import {FieldProps} from 'sanity'
+
+export function FormField(props: FieldProps & {testId: string}) {
+ const {testId} = props
+
+ return {props.renderDefault(props)}
+}
diff --git a/dev/studio-e2e-testing/components-api/FormInput.tsx b/dev/studio-e2e-testing/components-api/FormInput.tsx
new file mode 100644
index 00000000000..5d1f9dcafbd
--- /dev/null
+++ b/dev/studio-e2e-testing/components-api/FormInput.tsx
@@ -0,0 +1,10 @@
+import {Stack} from '@sanity/ui'
+import {InputProps} from 'sanity'
+
+export function FormInput(props: InputProps & {testId: string}) {
+ const {testId} = props
+
+ if (props.id === 'root') return props.renderDefault(props)
+
+ return {props.renderDefault(props)}
+}
diff --git a/dev/studio-e2e-testing/components-api/StudioLayout.tsx b/dev/studio-e2e-testing/components-api/StudioLayout.tsx
new file mode 100644
index 00000000000..0f85015ae97
--- /dev/null
+++ b/dev/studio-e2e-testing/components-api/StudioLayout.tsx
@@ -0,0 +1,12 @@
+import {Flex} from '@sanity/ui'
+import {LayoutProps} from 'sanity'
+
+export function StudioLayout(props: LayoutProps & {testId: string}) {
+ const {testId} = props
+
+ return (
+
+ {props.renderDefault(props)}
+
+ )
+}
diff --git a/dev/studio-e2e-testing/components-api/StudioLogo.tsx b/dev/studio-e2e-testing/components-api/StudioLogo.tsx
new file mode 100644
index 00000000000..cd1dc396216
--- /dev/null
+++ b/dev/studio-e2e-testing/components-api/StudioLogo.tsx
@@ -0,0 +1,9 @@
+import {Box} from '@sanity/ui'
+import React from 'react'
+import {LogoProps} from 'sanity'
+
+export function StudioLogo(props: LogoProps & {testId: string}) {
+ const {testId} = props
+
+ return {props.renderDefault(props)}
+}
diff --git a/dev/studio-e2e-testing/components-api/StudioNavbar.tsx b/dev/studio-e2e-testing/components-api/StudioNavbar.tsx
new file mode 100644
index 00000000000..69866c5b007
--- /dev/null
+++ b/dev/studio-e2e-testing/components-api/StudioNavbar.tsx
@@ -0,0 +1,8 @@
+import {Stack} from '@sanity/ui'
+import {NavbarProps} from 'sanity'
+
+export function StudioNavbar(props: NavbarProps & {testId: string}) {
+ const {testId} = props
+
+ return {props.renderDefault(props)}
+}
diff --git a/dev/studio-e2e-testing/components-api/StudioToolMenu.tsx b/dev/studio-e2e-testing/components-api/StudioToolMenu.tsx
new file mode 100644
index 00000000000..84bc17a6b3e
--- /dev/null
+++ b/dev/studio-e2e-testing/components-api/StudioToolMenu.tsx
@@ -0,0 +1,8 @@
+import {Stack} from '@sanity/ui'
+import {ToolMenuProps} from 'sanity'
+
+export function StudioToolMenu(props: ToolMenuProps & {testId: string}) {
+ const {testId} = props
+
+ return {props.renderDefault(props)}
+}
diff --git a/dev/studio-e2e-testing/components-api/index.tsx b/dev/studio-e2e-testing/components-api/index.tsx
new file mode 100644
index 00000000000..ac797358e66
--- /dev/null
+++ b/dev/studio-e2e-testing/components-api/index.tsx
@@ -0,0 +1,68 @@
+import {definePlugin} from 'sanity'
+import {StudioLayout} from './StudioLayout'
+import {StudioNavbar} from './StudioNavbar'
+import {StudioLogo} from './StudioLogo'
+import {StudioToolMenu} from './StudioToolMenu'
+import {FormInput} from './FormInput'
+import {FormField} from './FormField'
+import {DocumentLayout} from './DocumentLayout'
+
+const childComponents = definePlugin({
+ name: 'child-components',
+
+ document: {
+ components: {
+ unstable_layout: (props) => (
+
+ ),
+ },
+ },
+
+ form: {
+ components: {
+ input: (props) => ,
+ field: (props) => ,
+ },
+ },
+
+ studio: {
+ components: {
+ layout: (props) => ,
+ logo: (props) => ,
+ navbar: (props) => ,
+ toolMenu: (props) => (
+
+ ),
+ },
+ },
+})
+
+export const customComponents = definePlugin({
+ name: 'custom-components',
+
+ document: {
+ components: {
+ unstable_layout: (props) => (
+
+ ),
+ },
+ },
+
+ form: {
+ components: {
+ input: (props) => ,
+ field: (props) => ,
+ },
+ },
+
+ studio: {
+ components: {
+ layout: (props) => ,
+ logo: (props) => ,
+ navbar: (props) => ,
+ toolMenu: (props) => ,
+ },
+ },
+
+ plugins: [childComponents()],
+})
diff --git a/dev/studio-e2e-testing/components/Branding.tsx b/dev/studio-e2e-testing/components/Branding.tsx
deleted file mode 100644
index 39b34e83225..00000000000
--- a/dev/studio-e2e-testing/components/Branding.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import {Box, Text} from '@sanity/ui'
-import React from 'react'
-
-export function Branding() {
- return (
-
- E2E Test Studio™
-
- )
-}
diff --git a/dev/studio-e2e-testing/sanity.config.ts b/dev/studio-e2e-testing/sanity.config.ts
index 78a58de96c8..1dd07b3e960 100644
--- a/dev/studio-e2e-testing/sanity.config.ts
+++ b/dev/studio-e2e-testing/sanity.config.ts
@@ -14,8 +14,8 @@ import {copyAction} from 'sanity-test-studio/fieldActions/copyAction'
import {assistFieldActionGroup} from 'sanity-test-studio/fieldActions/assistFieldActionGroup'
import {customInspector} from 'sanity-test-studio/inspectors/custom'
import {pasteAction} from 'sanity-test-studio/fieldActions/pasteAction'
-import {Branding} from './components/Branding'
import {schemaTypes} from './schemas'
+import {customComponents} from './components-api'
const sharedSettings = definePlugin({
name: 'sharedSettings',
@@ -28,11 +28,7 @@ const sharedSettings = definePlugin({
assetSources: [imageAssetSource],
},
},
- studio: {
- components: {
- logo: Branding,
- },
- },
+
document: {
actions: documentActions,
inspectors: (prev, ctx) => {
@@ -52,6 +48,7 @@ const sharedSettings = definePlugin({
newDocumentOptions,
},
plugins: [
+ customComponents(),
deskTool({
icon: BookIcon,
name: 'content',
diff --git a/packages/sanity/src/core/config/components/useMiddlewareComponents.ts b/packages/sanity/src/core/config/components/useMiddlewareComponents.ts
index c01dbb5393c..5f08e4feeaa 100644
--- a/packages/sanity/src/core/config/components/useMiddlewareComponents.ts
+++ b/packages/sanity/src/core/config/components/useMiddlewareComponents.ts
@@ -34,7 +34,27 @@ function _createMiddlewareComponent(
}
}
-/** @internal */
+/**
+ * @internal
+ * This hook returns a component based on the Components API middleware chain.
+ *
+ * - The `pick` function is used to select a component from the provided plugin options in the configuration.
+ * - The `defaultComponent` is the default component that gets rendered with `renderDefault`.
+ * The `renderDefault` function is added to the props of the middleware components so that they can render the default
+ * component and continue the middleware chain.
+ *
+ * @example
+ * Example usage of:
+ *
+ * ```ts
+ * const StudioLayout = useMiddlewareComponents({
+ * pick: (plugin) => plugin.studio?.components?.layout,
+ * defaultComponent: StudioLayout,
+ * })
+ *
+ * return
+ *```
+ */
export function useMiddlewareComponents(props: {
pick: (plugin: PluginOptions) => ComponentType
defaultComponent: ComponentType
diff --git a/packages/sanity/src/core/config/form/index.ts b/packages/sanity/src/core/config/form/index.ts
new file mode 100644
index 00000000000..c9f6f047dc0
--- /dev/null
+++ b/packages/sanity/src/core/config/form/index.ts
@@ -0,0 +1 @@
+export * from './types'
diff --git a/packages/sanity/src/core/config/form/types.ts b/packages/sanity/src/core/config/form/types.ts
new file mode 100644
index 00000000000..6bbdf546392
--- /dev/null
+++ b/packages/sanity/src/core/config/form/types.ts
@@ -0,0 +1,16 @@
+import {ComponentType} from 'react'
+import {PreviewProps} from '../../components'
+import {InputProps, FieldProps, ItemProps, BlockProps, BlockAnnotationProps} from '../../form'
+
+/**
+ * @hidden
+ * @beta */
+export interface FormComponents {
+ annotation?: ComponentType
+ block?: ComponentType
+ field?: ComponentType
+ inlineBlock?: ComponentType
+ input?: ComponentType
+ item?: ComponentType
+ preview?: ComponentType
+}
diff --git a/packages/sanity/src/core/config/index.ts b/packages/sanity/src/core/config/index.ts
index 5d157c3e1d2..b039a78161c 100644
--- a/packages/sanity/src/core/config/index.ts
+++ b/packages/sanity/src/core/config/index.ts
@@ -11,3 +11,4 @@ export * from './useConfigContextFromSource'
export * from './components'
export * from './studio'
export * from './flattenConfig'
+export * from './form'
diff --git a/packages/sanity/src/core/config/types.ts b/packages/sanity/src/core/config/types.ts
index 0457f622c33..b520d0fb48e 100644
--- a/packages/sanity/src/core/config/types.ts
+++ b/packages/sanity/src/core/config/types.ts
@@ -12,18 +12,9 @@ import type {
import type {ComponentType, ReactNode} from 'react'
import type {Observable} from 'rxjs'
import type {i18n} from 'i18next'
-import type {
- BlockAnnotationProps,
- BlockProps,
- FieldProps,
- FormBuilderCustomMarkersComponent,
- FormBuilderMarkersComponent,
- InputProps,
- ItemProps,
-} from '../form'
+import type {FormBuilderCustomMarkersComponent, FormBuilderMarkersComponent} from '../form'
import type {LocalePluginOptions, LocaleSource} from '../i18n/types'
import type {InitialValueTemplateItem, Template, TemplateItem} from '../templates'
-import type {PreviewProps} from '../components/previews'
import type {AuthStore} from '../store'
import type {StudioTheme} from '../theme'
import type {SearchFilterDefinition} from '../studio/components/navbar/search/definitions/filters'
@@ -38,6 +29,7 @@ import type {
DocumentFieldActionsResolverContext,
DocumentInspector,
} from './document'
+import {FormComponents} from './form'
import type {Router, RouterState} from 'sanity/router'
/**
@@ -61,19 +53,14 @@ export interface SanityFormConfig {
CustomMarkers?: FormBuilderCustomMarkersComponent
Markers?: FormBuilderMarkersComponent
}
+
/**
+ * Components for the form.
* @hidden
* @beta
*/
- components?: {
- input?: ComponentType
- field?: ComponentType
- item?: ComponentType
- preview?: ComponentType
- block?: ComponentType
- inlineBlock?: ComponentType
- annotation?: ComponentType
- }
+ components?: FormComponents
+
file?: {
/**
* @hidden
@@ -273,6 +260,13 @@ export type NewDocumentCreationContext =
export interface DocumentPluginOptions {
badges?: DocumentBadgeComponent[] | DocumentBadgesResolver
actions?: DocumentActionComponent[] | DocumentActionsResolver
+
+ /**
+ * Components for the document.
+ * @internal
+ */
+ components?: DocumentComponents
+
/** @internal */
unstable_fieldActions?: DocumentFieldAction[] | DocumentFieldActionsResolver
/** @hidden @beta */
@@ -360,9 +354,16 @@ export interface PluginOptions {
document?: DocumentPluginOptions
tools?: Tool[] | ComposableOption
form?: SanityFormConfig
+
studio?: {
+ /**
+ * Components for the studio.
+ * @hidden
+ * @beta
+ */
components?: StudioComponentsPluginOptions
}
+
/** @beta @hidden */
i18n?: LocalePluginOptions
}
@@ -496,6 +497,24 @@ export type PartialContext = Pick<
Exclude
>
+/** @internal*/
+export interface DocumentLayoutProps {
+ /**
+ * The ID of the document. This is a read-only property and changing it will have no effect.
+ */
+ documentId: string
+ /**
+ * The type of the document. This is a read-only property and changing it will have no effect.
+ */
+ documentType: string
+ renderDefault: (props: DocumentLayoutProps) => React.ReactElement
+}
+
+interface DocumentComponents {
+ /** @internal */
+ unstable_layout?: ComponentType
+}
+
/** @public */
export interface SourceClientOptions {
/**
@@ -559,6 +578,12 @@ export interface Source {
*/
badges: (props: PartialContext) => DocumentBadgeComponent[]
+ /**
+ * Components for the document.
+ * @internal
+ */
+ components?: DocumentComponents
+
/** @internal */
unstable_fieldActions: (
props: PartialContext,
@@ -634,12 +659,7 @@ export interface Source {
* @hidden
* @beta
*/
- components?: {
- input?: ComponentType>
- field?: ComponentType>
- item?: ComponentType>
- preview?: ComponentType>
- }
+ components?: FormComponents
/**
* these have not been migrated over and are not merged by the form builder
@@ -659,6 +679,7 @@ export interface Source {
*/
studio?: {
/**
+ * Components for the studio.
* @hidden
* @beta
*/
diff --git a/packages/sanity/src/core/studio/components/navbar/StudioLogo.tsx b/packages/sanity/src/core/studio/components/navbar/StudioLogo.tsx
index 0bbf6dc826e..e9822631a1a 100644
--- a/packages/sanity/src/core/studio/components/navbar/StudioLogo.tsx
+++ b/packages/sanity/src/core/studio/components/navbar/StudioLogo.tsx
@@ -9,7 +9,7 @@ export function StudioLogo(props: LogoProps) {
const {title} = props
return (
-
+
{title}
)
diff --git a/packages/sanity/src/core/studio/components/navbar/StudioNavbar.tsx b/packages/sanity/src/core/studio/components/navbar/StudioNavbar.tsx
index 89f1ac1430e..a70cfb6a541 100644
--- a/packages/sanity/src/core/studio/components/navbar/StudioNavbar.tsx
+++ b/packages/sanity/src/core/studio/components/navbar/StudioNavbar.tsx
@@ -144,7 +144,7 @@ export function StudioNavbar() {
({
'aria-label': openDialogAriaLabel,
+ 'data-testid': 'new-document-button',
disabled: disabled || loading,
icon: AddIcon,
mode: 'bleed',
diff --git a/packages/sanity/src/desk/comments/plugin/document-layout/CommentsDocumentLayout.tsx b/packages/sanity/src/desk/comments/plugin/document-layout/CommentsDocumentLayout.tsx
new file mode 100644
index 00000000000..80b62405698
--- /dev/null
+++ b/packages/sanity/src/desk/comments/plugin/document-layout/CommentsDocumentLayout.tsx
@@ -0,0 +1,48 @@
+import React, {useCallback} from 'react'
+import {
+ CommentsEnabledProvider,
+ CommentsProvider,
+ CommentsSelectedPathProvider,
+ useCommentsEnabled,
+} from '../../src'
+import {useDocumentPane} from '../../..'
+import {COMMENTS_INSPECTOR_NAME} from '../../../panes/document/constants'
+import {DocumentLayoutProps} from 'sanity'
+
+export function CommentsDocumentLayout(props: DocumentLayoutProps) {
+ const {documentId, documentType} = props
+
+ return (
+
+
+
+ )
+}
+
+function CommentsDocumentLayoutInner(props: DocumentLayoutProps) {
+ const {documentId, documentType} = props
+ const commentsEnabled = useCommentsEnabled()
+ const {openInspector, inspector} = useDocumentPane()
+
+ const handleOpenCommentsInspector = useCallback(() => {
+ if (inspector?.name === COMMENTS_INSPECTOR_NAME) return
+
+ openInspector(COMMENTS_INSPECTOR_NAME)
+ }, [inspector?.name, openInspector])
+
+ // If comments are not enabled, render the default document layout
+ if (!commentsEnabled) {
+ return props.renderDefault(props)
+ }
+
+ return (
+
+ {props.renderDefault(props)}
+
+ )
+}
diff --git a/packages/sanity/src/desk/comments/plugin/document-layout/index.ts b/packages/sanity/src/desk/comments/plugin/document-layout/index.ts
new file mode 100644
index 00000000000..83ed9e8c602
--- /dev/null
+++ b/packages/sanity/src/desk/comments/plugin/document-layout/index.ts
@@ -0,0 +1 @@
+export * from './CommentsDocumentLayout'
diff --git a/packages/sanity/src/desk/comments/plugin/field/CommentField.tsx b/packages/sanity/src/desk/comments/plugin/field/CommentsField.tsx
similarity index 98%
rename from packages/sanity/src/desk/comments/plugin/field/CommentField.tsx
rename to packages/sanity/src/desk/comments/plugin/field/CommentsField.tsx
index 630910678f8..c68f14d521b 100644
--- a/packages/sanity/src/desk/comments/plugin/field/CommentField.tsx
+++ b/packages/sanity/src/desk/comments/plugin/field/CommentsField.tsx
@@ -13,7 +13,7 @@ import {
CommentCreatePayload,
useCommentsSelectedPath,
} from '../../src'
-import {CommentFieldButton} from './CommentFieldButton'
+import {CommentsFieldButton} from './CommentsFieldButton'
import {FieldProps, getSchemaTypeTitle, useCurrentUser} from 'sanity'
const HIGHLIGHT_BLOCK_VARIANTS: Variants = {
@@ -28,7 +28,7 @@ const HIGHLIGHT_BLOCK_VARIANTS: Variants = {
},
}
-export function CommentField(props: FieldProps) {
+export function CommentsField(props: FieldProps) {
const isEnabled = useCommentsEnabled()
if (isEnabled) return
@@ -233,7 +233,7 @@ function CommentFieldInner(props: FieldProps) {
const internalComments: FieldProps['__internal_comments'] = useMemo(
() => ({
button: currentUser && (
-
{props.renderDefault(props)}
diff --git a/packages/sanity/src/desk/comments/plugin/studio-layout/index.ts b/packages/sanity/src/desk/comments/plugin/studio-layout/index.ts
new file mode 100644
index 00000000000..6e525fb95ca
--- /dev/null
+++ b/packages/sanity/src/desk/comments/plugin/studio-layout/index.ts
@@ -0,0 +1 @@
+export * from './CommentsStudioLayout'
diff --git a/packages/sanity/src/desk/comments/src/context/enabled/CommentsEnabledProvider.tsx b/packages/sanity/src/desk/comments/src/context/enabled/CommentsEnabledProvider.tsx
index 23f5bc95bda..cf0221dd7fd 100644
--- a/packages/sanity/src/desk/comments/src/context/enabled/CommentsEnabledProvider.tsx
+++ b/packages/sanity/src/desk/comments/src/context/enabled/CommentsEnabledProvider.tsx
@@ -1,6 +1,6 @@
-import React, {useMemo} from 'react'
+import React from 'react'
+import {useResolveCommentsEnabled} from '../../hooks'
import {CommentsEnabledContext} from './CommentsEnabledContext'
-import {useFeatureEnabled, useSource, getPublishedId} from 'sanity'
interface CommentsEnabledProviderProps {
children: React.ReactNode
@@ -13,21 +13,7 @@ export const CommentsEnabledProvider = React.memo(function CommentsEnabledProvid
) {
const {children, documentId, documentType} = props
- // Check if the projects plan has the feature enabled
- const {enabled: featureEnabled, isLoading} = useFeatureEnabled('studioComments')
-
- const {enabled} = useSource().document.unstable_comments
-
- // Check if the feature is enabled for the current document in the config
- const enabledFromConfig = useMemo(
- () => enabled({documentType, documentId: getPublishedId(documentId)}),
- [documentId, documentType, enabled],
- )
-
- const isEnabled = useMemo((): boolean => {
- if (isLoading || !featureEnabled || !enabledFromConfig) return false
- return true
- }, [enabledFromConfig, featureEnabled, isLoading])
+ const isEnabled = useResolveCommentsEnabled(documentId, documentType)
return (
{children}
diff --git a/packages/sanity/src/desk/comments/src/hooks/index.ts b/packages/sanity/src/desk/comments/src/hooks/index.ts
index 6a540105c94..5d1ea823722 100644
--- a/packages/sanity/src/desk/comments/src/hooks/index.ts
+++ b/packages/sanity/src/desk/comments/src/hooks/index.ts
@@ -5,3 +5,4 @@ export * from './useCommentsSetup'
export * from './useCommentsEnabled'
export * from './useCommentsOnboarding'
export * from './useCommentsSelectedPath'
+export * from './useResolveCommentsEnabled'
diff --git a/packages/sanity/src/desk/comments/src/hooks/useCommentsEnabled.ts b/packages/sanity/src/desk/comments/src/hooks/useCommentsEnabled.ts
index 9c67673cbbe..935f51d1b84 100644
--- a/packages/sanity/src/desk/comments/src/hooks/useCommentsEnabled.ts
+++ b/packages/sanity/src/desk/comments/src/hooks/useCommentsEnabled.ts
@@ -8,7 +8,11 @@ import {CommentsEnabledContext} from '../context/enabled'
* if comments is enabled for the current document in the config API.
*/
export function useCommentsEnabled(): boolean {
- const enabled = useContext(CommentsEnabledContext)
+ const ctx = useContext(CommentsEnabledContext)
- return Boolean(enabled)
+ if (ctx === null) {
+ throw new Error('useCommentsEnabled: missing context value')
+ }
+
+ return ctx
}
diff --git a/packages/sanity/src/desk/comments/src/hooks/useResolveCommentsEnabled.ts b/packages/sanity/src/desk/comments/src/hooks/useResolveCommentsEnabled.ts
new file mode 100644
index 00000000000..65c148c1b6d
--- /dev/null
+++ b/packages/sanity/src/desk/comments/src/hooks/useResolveCommentsEnabled.ts
@@ -0,0 +1,24 @@
+import {useMemo} from 'react'
+import {getPublishedId, useFeatureEnabled, useSource} from 'sanity'
+
+/**
+ * @internal
+ * A hook that resolves if comments are enabled for the current document and document type
+ * and if the feature is enabled for the current project.
+ */
+export function useResolveCommentsEnabled(documentId: string, documentType: string): boolean {
+ // Check if the projects plan has the feature enabled
+ const {enabled: featureEnabled, isLoading} = useFeatureEnabled('studioComments')
+
+ const {enabled} = useSource().document.unstable_comments
+
+ // Check if the feature is enabled for the current document in the config
+ const enabledFromConfig = useMemo(
+ () => enabled({documentType, documentId: getPublishedId(documentId)}),
+ [documentId, documentType, enabled],
+ )
+
+ const isEnabled = !isLoading && featureEnabled && enabledFromConfig
+
+ return isEnabled
+}
diff --git a/packages/sanity/src/desk/panes/document/DocumentPane.tsx b/packages/sanity/src/desk/panes/document/DocumentPane.tsx
index 8c60a178dc2..f5dbdb96fed 100644
--- a/packages/sanity/src/desk/panes/document/DocumentPane.tsx
+++ b/packages/sanity/src/desk/panes/document/DocumentPane.tsx
@@ -1,69 +1,28 @@
-import {
- Card,
- Code,
- DialogProvider,
- DialogProviderProps,
- Flex,
- PortalProvider,
- Stack,
- Text,
- useElementRect,
-} from '@sanity/ui'
-import React, {memo, useCallback, useMemo, useState} from 'react'
-import styled from 'styled-components'
+import {Stack, Text} from '@sanity/ui'
+import {memo, useMemo} from 'react'
import {fromString as pathFromString} from '@sanity/util/paths'
import {Path} from '@sanity/types'
import {DocumentPaneNode} from '../../types'
-import {Pane, PaneFooter, usePaneRouter} from '../../components'
-import {usePaneLayout} from '../../components/pane/usePaneLayout'
+import {usePaneRouter} from '../../components'
import {ErrorPane} from '../error'
import {LoadingPane} from '../loading'
-import {DOCUMENT_PANEL_PORTAL_ELEMENT} from '../../constants'
-import {DocumentOperationResults} from './DocumentOperationResults'
-import {DocumentPaneProvider} from './DocumentPaneProvider'
-import {DocumentPanel} from './documentPanel'
-import {DocumentActionShortcuts} from './keyboardShortcuts'
-import {DocumentStatusBar} from './statusBar'
-import {DocumentPaneProviderProps} from './types'
-import {useDocumentPane} from './useDocumentPane'
-import {
- DOCUMENT_INSPECTOR_MIN_WIDTH,
- DOCUMENT_PANEL_INITIAL_MIN_WIDTH,
- DOCUMENT_PANEL_MIN_WIDTH,
-} from './constants'
import {structureLocaleNamespace} from '../../i18n'
+import {DocumentPaneProviderProps} from './types'
+import {DocumentPaneProvider} from './DocumentPaneProvider'
+import {useDocumentLayoutComponent} from './document-layout'
import {
- ChangeConnectorRoot,
ReferenceInputOptionsProvider,
SourceProvider,
- isDev,
Translate,
useDocumentType,
useSource,
useTemplatePermissions,
useTemplates,
useTranslation,
- useZIndex,
} from 'sanity'
-import {CommentsEnabledProvider} from '../../comments'
type DocumentPaneOptions = DocumentPaneNode['options']
-const DIALOG_PROVIDER_POSITION: DialogProviderProps['position'] = [
- // We use the `position: fixed` for dialogs on narrower screens (first two media breakpoints).
- 'fixed',
- 'fixed',
- // And we use the `position: absolute` strategy (within panes) on wide screens.
- 'absolute',
-]
-
-const StyledChangeConnectorRoot = styled(ChangeConnectorRoot)`
- flex: 1;
- display: flex;
- flex-direction: column;
- min-height: 0;
- min-width: 0;
-`
/**
* @internal
*/
@@ -84,6 +43,8 @@ function DocumentPaneInner(props: DocumentPaneProviderProps) {
const options = usePaneOptions(pane.options, paneRouter.params)
const {documentType, isLoaded: isDocumentLoaded} = useDocumentType(options.id, options.type)
+ const DocumentLayout = useDocumentLayoutComponent()
+
// The templates that should be creatable from inside this document pane.
// For example, from the "Create new" menu in reference inputs.
const templateItems = useMemo(() => {
@@ -160,26 +121,24 @@ function DocumentPaneInner(props: DocumentPaneProviderProps) {
}
return (
-
-
+ {/* NOTE: this is a temporary location for this provider until we */}
+ {/* stabilize the reference input options formally in the form builder */}
+ {/* eslint-disable-next-line react/jsx-pascal-case */}
+
- {/* NOTE: this is a temporary location for this provider until we */}
- {/* stabilize the reference input options formally in the form builder */}
- {/* eslint-disable-next-line react/jsx-pascal-case */}
-
-
-
-
-
+
+
+
)
}
@@ -223,137 +182,3 @@ function mergeDocumentType(
},
}
}
-
-function InnerDocumentPane() {
- const {
- changesOpen,
- documentType,
- inspector,
- inspectOpen,
- onFocus,
- onPathOpen,
- onHistoryOpen,
- onKeyUp,
- paneKey,
- schemaType,
- value,
- } = useDocumentPane()
- const {collapsed: layoutCollapsed} = usePaneLayout()
- const zOffsets = useZIndex()
- const [rootElement, setRootElement] = useState(null)
- const [footerElement, setFooterElement] = useState(null)
- const [actionsBoxElement, setActionsBoxElement] = useState(null)
- const [documentPanelPortalElement, setDocumentPanelPortalElement] = useState(
- null,
- )
- const footerRect = useElementRect(footerElement)
- const footerH = footerRect?.height
-
- const onConnectorSetFocus = useCallback(
- (path: Path) => {
- onPathOpen(path)
- onFocus(path)
- },
- [onPathOpen, onFocus],
- )
-
- const currentMinWidth =
- DOCUMENT_PANEL_INITIAL_MIN_WIDTH + (inspector ? DOCUMENT_INSPECTOR_MIN_WIDTH : 0)
-
- const minWidth = DOCUMENT_PANEL_MIN_WIDTH + (inspector ? DOCUMENT_INSPECTOR_MIN_WIDTH : 0)
- const {t} = useTranslation(structureLocaleNamespace)
-
- if (!schemaType) {
- return (
-
- }
- tone="caution"
- >
-
- {documentType && (
-
-
-
- )}
-
- {!documentType && (
- {t('panes.document-pane.document-unknown-type.without-schema.text')}
- )}
-
- {isDev && value && (
- /* eslint-disable i18next/no-literal-string */
- <>
- Here is the JSON representation of the document:
-
-
- {JSON.stringify(value, null, 2)}
-
-
- >
- /* eslint-enable i18next/no-literal-string */
- )}
-
-
- )
- }
-
- return (
-
-
-
-
-
-
-
-
-
- {/* These providers are added because we want the dialogs in `DocumentStatusBar` to be scoped to the document pane. */}
- {/* The portal element comes from `DocumentPanel`. */}
-
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/packages/sanity/src/desk/panes/document/DocumentPaneContext.ts b/packages/sanity/src/desk/panes/document/DocumentPaneContext.ts
index 29312ad169b..1d6f34b6022 100644
--- a/packages/sanity/src/desk/panes/document/DocumentPaneContext.ts
+++ b/packages/sanity/src/desk/panes/document/DocumentPaneContext.ts
@@ -16,7 +16,6 @@ import {
DocumentFormNode,
DocumentInspector,
DocumentLanguageFilterComponent,
- DocumentPermission,
EditStateFor,
PatchEvent,
PermissionCheckResult,
@@ -47,14 +46,12 @@ export interface DocumentPaneContextValue {
inspector: DocumentInspector | null
inspectors: DocumentInspector[]
menuItemGroups: PaneMenuItemGroup[]
- menuItems: PaneMenuItem[]
onBlur: (blurredPath: Path) => void
onChange: (event: PatchEvent) => void
onFocus: (pathOrEvent: Path) => void
onHistoryClose: () => void
onHistoryOpen: () => void
onInspectClose: () => void
- onKeyUp: (event: React.KeyboardEvent) => void
onMenuAction: (item: PaneMenuItem) => void
onPaneClose: () => void
onPaneSplit?: () => void
diff --git a/packages/sanity/src/desk/panes/document/DocumentPaneProvider.tsx b/packages/sanity/src/desk/panes/document/DocumentPaneProvider.tsx
index 9734b019cab..01685be9ccf 100644
--- a/packages/sanity/src/desk/panes/document/DocumentPaneProvider.tsx
+++ b/packages/sanity/src/desk/panes/document/DocumentPaneProvider.tsx
@@ -1,39 +1,30 @@
-import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
+import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
import type {ObjectSchemaType, Path, SanityDocument, SanityDocumentLike} from '@sanity/types'
import {omit} from 'lodash'
import {useToast} from '@sanity/ui'
import {fromString as pathFromString, resolveKeyedPath} from '@sanity/util/paths'
-import isHotkey from 'is-hotkey'
import {isActionEnabled} from '@sanity/schema/_internal'
import {usePaneRouter} from '../../components'
import type {PaneMenuItem} from '../../types'
import {useDeskTool} from '../../useDeskTool'
import {structureLocaleNamespace} from '../../i18n'
-import {CommentsProvider, CommentsSelectedPathProvider, useCommentsEnabled} from '../../comments'
import {DocumentPaneContext, type DocumentPaneContextValue} from './DocumentPaneContext'
-import {getMenuItems} from './menuItems'
import type {DocumentPaneProviderProps} from './types'
import {usePreviewUrl} from './usePreviewUrl'
import {getInitialValueTemplateOpts} from './getInitialValueTemplateOpts'
import {
- COMMENTS_INSPECTOR_NAME,
DEFAULT_MENU_ITEM_GROUPS,
EMPTY_PARAMS,
HISTORY_INSPECTOR_NAME,
INSPECT_ACTION_PREFIX,
} from './constants'
-import {DocumentInspectorMenuItemsResolver} from './DocumentInspectorMenuItemsResolver'
import {
type DocumentFieldAction,
- type DocumentFieldActionNode,
type DocumentInspector,
- type DocumentInspectorMenuItem,
type DocumentPresence,
type PatchEvent,
type StateTree,
EMPTY_ARRAY,
- FieldActionsProvider,
- FieldActionsResolver,
getDraftId,
getExpandOperations,
getPublishedId,
@@ -114,8 +105,6 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
const value: SanityDocumentLike = editState?.draft || editState?.published || initialValue.value
const [isDeleting, setIsDeleting] = useState(false)
- const [inspectorMenuItems, setInspectorMenuItems] = useState([])
-
// Resolve document actions
const actions = useMemo(
() => documentActions({schemaType: documentType, documentId}),
@@ -220,21 +209,8 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
const changesOpen = currentInspector?.name === HISTORY_INSPECTOR_NAME
- const hasValue = Boolean(value)
const {t} = useTranslation(structureLocaleNamespace)
- const menuItems = useMemo(
- () =>
- getMenuItems({
- currentInspector,
- features,
- hasValue,
- inspectorMenuItems,
- inspectors,
- previewUrl,
- t,
- }),
- [currentInspector, features, hasValue, inspectorMenuItems, inspectors, previewUrl, t],
- )
+
const inspectOpen = params.inspect === 'on'
const compareValue: Partial | null = changesOpen
? sinceAttributes
@@ -473,22 +449,6 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
],
)
- const handleKeyUp = useCallback(
- (event: React.KeyboardEvent) => {
- for (const item of menuItems) {
- if (item.shortcut) {
- if (isHotkey(item.shortcut, event)) {
- event.preventDefault()
- event.stopPropagation()
- handleMenuAction(item)
- return
- }
- }
- }
- },
- [handleMenuAction, menuItems],
- )
-
const handleLegacyInspectClose = useCallback(
() => toggleLegacyInspect(false),
[toggleLegacyInspect],
@@ -616,7 +576,6 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
focusPath,
inspector: currentInspector || null,
inspectors,
- menuItems,
onBlur: handleBlur,
onChange: handleChange,
onFocus: handleFocus,
@@ -624,7 +583,6 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
onHistoryClose: handleHistoryClose,
onHistoryOpen: handleHistoryOpen,
onInspectClose: handleLegacyInspectClose,
- onKeyUp: handleKeyUp,
onMenuAction: handleMenuAction,
onPaneClose: handlePaneClose,
onPaneSplit: handlePaneSplit,
@@ -699,66 +657,8 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
return undefined
}, [params, documentId, onFocusPath, setOpenPath, ready, paneRouter])
- const [rootFieldActionNodes, setRootFieldActionNodes] = useState([])
-
- const commentsEnabled = useCommentsEnabled()
-
- const handleOpenCommentsInspector = useCallback(() => {
- if (currentInspector?.name === COMMENTS_INSPECTOR_NAME) return
-
- openInspector(COMMENTS_INSPECTOR_NAME)
- }, [currentInspector?.name, openInspector])
-
- const content = useMemo(() => {
- // If comments are not enabled, return children as-is without wrapping in providers
- if (!commentsEnabled) return children
-
- return (
-
- {children}
-
- )
- }, [
- children,
- commentsEnabled,
- currentInspector?.name,
- documentId,
- documentType,
- handleOpenCommentsInspector,
- ])
-
return (
-
- {inspectors.length > 0 && (
-
- )}
-
- {/* Resolve root-level field actions */}
- {fieldActions.length > 0 && schemaType && (
-
- )}
-
-
- {content}
-
-
+ {children}
)
})
diff --git a/packages/sanity/src/desk/panes/document/document-layout/DocumentLayout.tsx b/packages/sanity/src/desk/panes/document/document-layout/DocumentLayout.tsx
new file mode 100644
index 00000000000..5a47189f9a1
--- /dev/null
+++ b/packages/sanity/src/desk/panes/document/document-layout/DocumentLayout.tsx
@@ -0,0 +1,226 @@
+import {useElementRect, DialogProvider, Flex, PortalProvider, DialogProviderProps} from '@sanity/ui'
+import {useState, useCallback, useMemo} from 'react'
+import {useTranslation} from 'react-i18next'
+import {Path} from 'sanity-diff-patch'
+import styled from 'styled-components'
+import isHotkey from 'is-hotkey'
+import {usePaneLayout, Pane, PaneFooter} from '../../../components'
+import {DOCUMENT_PANEL_PORTAL_ELEMENT} from '../../../constants'
+import {structureLocaleNamespace} from '../../../i18n'
+import {useDeskTool} from '../../../useDeskTool'
+import {DocumentOperationResults} from '../DocumentOperationResults'
+import {
+ DOCUMENT_PANEL_INITIAL_MIN_WIDTH,
+ DOCUMENT_INSPECTOR_MIN_WIDTH,
+ DOCUMENT_PANEL_MIN_WIDTH,
+} from '../constants'
+import {DocumentPanel} from '../documentPanel'
+import {DocumentActionShortcuts} from '../keyboardShortcuts'
+import {DocumentStatusBar} from '../statusBar'
+import {useDocumentPane} from '../useDocumentPane'
+import {DocumentPanelHeader} from '../documentPanel/header'
+import {DocumentInspectorMenuItemsResolver} from '../DocumentInspectorMenuItemsResolver'
+import {usePreviewUrl} from '../usePreviewUrl'
+import {getMenuItems} from '../menuItems'
+import {DocumentLayoutError} from './DocumentLayoutError'
+import {
+ DocumentLayoutProps,
+ useZIndex,
+ ChangeConnectorRoot,
+ DocumentInspectorMenuItem,
+ FieldActionsResolver,
+ DocumentFieldActionNode,
+ FieldActionsProvider,
+} from 'sanity'
+
+const EMPTY_ARRAY: [] = []
+
+const DIALOG_PROVIDER_POSITION: DialogProviderProps['position'] = [
+ // We use the `position: fixed` for dialogs on narrower screens (first two media breakpoints).
+ 'fixed',
+ 'fixed',
+ // And we use the `position: absolute` strategy (within panes) on wide screens.
+ 'absolute',
+]
+
+const StyledChangeConnectorRoot = styled(ChangeConnectorRoot)`
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ min-width: 0;
+`
+
+export function DocumentLayout() {
+ const {
+ changesOpen,
+ documentId,
+ documentType,
+ fieldActions,
+ inspectOpen,
+ inspector,
+ inspectors,
+ onFocus,
+ onHistoryOpen,
+ onMenuAction,
+ onPathOpen,
+ paneKey,
+ schemaType,
+ value,
+ } = useDocumentPane()
+
+ const {features} = useDeskTool()
+ const {t} = useTranslation(structureLocaleNamespace)
+ const {collapsed: layoutCollapsed} = usePaneLayout()
+ const zOffsets = useZIndex()
+ const previewUrl = usePreviewUrl(value)
+
+ const [rootElement, setRootElement] = useState(null)
+ const [footerElement, setFooterElement] = useState(null)
+ const [headerElement, setHeaderElement] = useState(null)
+
+ const [actionsBoxElement, setActionsBoxElement] = useState(null)
+ const [documentPanelPortalElement, setDocumentPanelPortalElement] = useState(
+ null,
+ )
+
+ const [inspectorMenuItems, setInspectorMenuItems] = useState([])
+ const [rootFieldActionNodes, setRootFieldActionNodes] = useState([])
+
+ const footerRect = useElementRect(footerElement)
+ const headerRect = useElementRect(headerElement)
+ const footerHeight = footerRect?.height
+ const headerHeight = headerRect?.height
+ const currentMinWidth =
+ DOCUMENT_PANEL_INITIAL_MIN_WIDTH + (inspector ? DOCUMENT_INSPECTOR_MIN_WIDTH : 0)
+ const minWidth = DOCUMENT_PANEL_MIN_WIDTH + (inspector ? DOCUMENT_INSPECTOR_MIN_WIDTH : 0)
+
+ const currentInspector = useMemo(
+ () => inspectors?.find((i) => i.name === inspector?.name),
+ [inspectors, inspector?.name],
+ )
+
+ const hasValue = Boolean(value)
+
+ const menuItems = useMemo(
+ () =>
+ getMenuItems({
+ currentInspector,
+ features,
+ hasValue,
+ inspectorMenuItems,
+ inspectors,
+ previewUrl,
+ t,
+ }),
+ [currentInspector, features, hasValue, inspectorMenuItems, inspectors, previewUrl, t],
+ )
+
+ const handleKeyUp = useCallback(
+ (event: React.KeyboardEvent) => {
+ for (const item of menuItems) {
+ if (item.shortcut) {
+ if (isHotkey(item.shortcut, event)) {
+ event.preventDefault()
+ event.stopPropagation()
+ onMenuAction(item)
+ return
+ }
+ }
+ }
+ },
+ [onMenuAction, menuItems],
+ )
+
+ const onConnectorSetFocus = useCallback(
+ (path: Path) => {
+ onPathOpen(path)
+ onFocus(path)
+ },
+ [onPathOpen, onFocus],
+ )
+
+ if (!schemaType) {
+ return (
+
+ )
+ }
+
+ return (
+ <>
+ {inspectors.length > 0 && (
+
+ )}
+
+ {fieldActions.length > 0 && schemaType && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* These providers are added because we want the dialogs in `DocumentStatusBar` to be scoped to the document pane. */}
+ {/* The portal element comes from `DocumentPanel`. */}
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/packages/sanity/src/desk/panes/document/document-layout/DocumentLayoutError.tsx b/packages/sanity/src/desk/panes/document/document-layout/DocumentLayoutError.tsx
new file mode 100644
index 00000000000..6516c9cd999
--- /dev/null
+++ b/packages/sanity/src/desk/panes/document/document-layout/DocumentLayoutError.tsx
@@ -0,0 +1,64 @@
+import {Card, Code, Stack, Text} from '@sanity/ui'
+import React from 'react'
+import {ErrorPane} from '../../error'
+import {Translate, isDev, useTranslation} from 'sanity'
+
+interface DocumentLayoutErrorProps {
+ currentMinWidth?: number
+ documentType?: string
+ minWidth?: number
+ paneKey: string
+ value?: Record
+}
+
+export function DocumentLayoutError(props: DocumentLayoutErrorProps) {
+ const {documentType, value, currentMinWidth, paneKey, minWidth} = props
+ const {t} = useTranslation()
+
+ return (
+
+ }
+ tone="caution"
+ >
+
+ {documentType && (
+
+
+
+ )}
+
+ {!documentType && (
+ {t('panes.document-pane.document-unknown-type.without-schema.text')}
+ )}
+
+ {isDev && value && (
+ /* eslint-disable i18next/no-literal-string */
+ <>
+ Here is the JSON representation of the document:
+
+
+
+ {JSON.stringify(value, null, 2)}
+
+
+ >
+ /* eslint-enable i18next/no-literal-string */
+ )}
+
+
+ )
+}
diff --git a/packages/sanity/src/desk/panes/document/document-layout/index.ts b/packages/sanity/src/desk/panes/document/document-layout/index.ts
new file mode 100644
index 00000000000..64577e3cf20
--- /dev/null
+++ b/packages/sanity/src/desk/panes/document/document-layout/index.ts
@@ -0,0 +1,2 @@
+export * from './DocumentLayout'
+export * from './useDocumentLayoutComponent'
diff --git a/packages/sanity/src/desk/panes/document/document-layout/useDocumentLayoutComponent.ts b/packages/sanity/src/desk/panes/document/document-layout/useDocumentLayoutComponent.ts
new file mode 100644
index 00000000000..9209cffa6ed
--- /dev/null
+++ b/packages/sanity/src/desk/panes/document/document-layout/useDocumentLayoutComponent.ts
@@ -0,0 +1,22 @@
+import {ComponentType} from 'react'
+import {DocumentLayout} from './DocumentLayout'
+import {DocumentLayoutProps, PluginOptions, useMiddlewareComponents} from 'sanity'
+
+function pick(plugin: PluginOptions) {
+ return plugin.document?.components?.unstable_layout as ComponentType<
+ Omit
+ >
+}
+
+/**
+ * A hook that returns the document layout composed
+ * by the Components API (`document.components.layout`).
+ */
+export function useDocumentLayoutComponent(): ComponentType<
+ Omit
+> {
+ return useMiddlewareComponents({
+ pick,
+ defaultComponent: DocumentLayout,
+ })
+}
diff --git a/packages/sanity/src/desk/panes/document/documentPanel/DocumentPanel.tsx b/packages/sanity/src/desk/panes/document/documentPanel/DocumentPanel.tsx
index 9a9bcc3ccca..f77cf107bd4 100644
--- a/packages/sanity/src/desk/panes/document/documentPanel/DocumentPanel.tsx
+++ b/packages/sanity/src/desk/panes/document/documentPanel/DocumentPanel.tsx
@@ -1,11 +1,4 @@
-import {
- BoundaryElementProvider,
- Flex,
- PortalProvider,
- usePortal,
- useElementRect,
- Box,
-} from '@sanity/ui'
+import {BoundaryElementProvider, Flex, PortalProvider, usePortal, Box} from '@sanity/ui'
import React, {createElement, useEffect, useMemo, useRef, useState} from 'react'
import styled, {css} from 'styled-components'
import {PaneContent, usePane, usePaneLayout} from '../../../components'
@@ -17,13 +10,13 @@ import {DeletedDocumentBanner} from './DeletedDocumentBanner'
import {ReferenceChangedBanner} from './ReferenceChangedBanner'
import {PermissionCheckBanner} from './PermissionCheckBanner'
import {FormView} from './documentViews'
-import {DocumentPanelHeader} from './header'
import {ScrollContainer, useTimelineSelector, VirtualizerScrollInstanceProvider} from 'sanity'
interface DocumentPanelProps {
footerHeight: number | null
- rootElement: HTMLDivElement | null
+ headerHeight: number | null
isInspectOpen: boolean
+ rootElement: HTMLDivElement | null
setDocumentPanelPortalElement: (el: HTMLElement | null) => void
}
@@ -46,7 +39,8 @@ const Scroller = styled(ScrollContainer)<{$disabled: boolean}>(({$disabled}) =>
})
export const DocumentPanel = function DocumentPanel(props: DocumentPanelProps) {
- const {footerHeight, isInspectOpen, rootElement, setDocumentPanelPortalElement} = props
+ const {footerHeight, headerHeight, isInspectOpen, rootElement, setDocumentPanelPortalElement} =
+ props
const {
activeViewId,
displayed,
@@ -67,8 +61,6 @@ export const DocumentPanel = function DocumentPanel(props: DocumentPanelProps) {
const {collapsed} = usePane()
const parentPortal = usePortal()
const {features} = useDeskTool()
- const [headerElement, setHeaderElement] = useState(null)
- const headerRect = useElementRect(headerElement)
const portalRef = useRef(null)
const [documentScrollElement, setDocumentScrollElement] = useState(null)
const formContainerElement = useRef(null)
@@ -88,11 +80,11 @@ export const DocumentPanel = function DocumentPanel(props: DocumentPanelProps) {
// Calculate the height of the header
const margins: [number, number, number, number] = useMemo(() => {
if (layoutCollapsed) {
- return [headerRect?.height || 0, 0, footerHeight ? footerHeight + 2 : 2, 0]
+ return [headerHeight || 0, 0, footerHeight ? footerHeight + 2 : 2, 0]
}
return [0, 0, 2, 0]
- }, [layoutCollapsed, footerHeight, headerRect])
+ }, [layoutCollapsed, footerHeight, headerHeight])
const formViewHidden = activeView.type !== 'form'
@@ -139,69 +131,65 @@ export const DocumentPanel = function DocumentPanel(props: DocumentPanelProps) {
const showInspector = Boolean(!collapsed && inspector)
return (
- <>
-
-
-
-
- {(features.resizablePanes || !showInspector) && (
-
-
-
-
- {activeView.type === 'form' && !isPermissionsLoading && ready && (
- <>
-
- {!isDeleting && isDeleted && (
-
- )}
-
- >
- )}
-
-
-
+
+ {(features.resizablePanes || !showInspector) && (
+
+
+
+
+ {activeView.type === 'form' && !isPermissionsLoading && ready && (
+ <>
+
- {activeViewNode}
-
-
- {inspectDialog}
-
-
-
-
-
-
- )}
-
- {showInspector && (
-
-
-
- )}
-
-
- >
+ {!isDeleting && isDeleted && (
+
+ )}
+
+ >
+ )}
+
+
+
+ {activeViewNode}
+
+
+ {inspectDialog}
+
+
+
+
+
+
+ )}
+
+ {showInspector && (
+
+
+
+ )}
+
+
)
}
diff --git a/packages/sanity/src/desk/panes/document/documentPanel/header/DocumentPanelHeader.tsx b/packages/sanity/src/desk/panes/document/documentPanel/header/DocumentPanelHeader.tsx
index d192a8869b1..f3f959a3ae9 100644
--- a/packages/sanity/src/desk/panes/document/documentPanel/header/DocumentPanelHeader.tsx
+++ b/packages/sanity/src/desk/panes/document/documentPanel/header/DocumentPanelHeader.tsx
@@ -12,24 +12,27 @@ import {TimelineMenu} from '../../timeline'
import {useDocumentPane} from '../../useDocumentPane'
import {isMenuNodeButton, isNotMenuNodeButton, resolveMenuNodes} from '../../../../menuNodes'
import {useDeskTool} from '../../../../useDeskTool'
+import {structureLocaleNamespace} from '../../../../i18n'
+import {PaneMenuItem} from '../../../../types'
import {DocumentHeaderTabs} from './DocumentHeaderTabs'
import {DocumentHeaderTitle} from './DocumentHeaderTitle'
-import {structureLocaleNamespace} from '../../../../i18n'
import {useFieldActions, useTimelineSelector, useTranslation} from 'sanity'
// eslint-disable-next-line @typescript-eslint/no-empty-interface
-export interface DocumentPanelHeaderProps {}
+export interface DocumentPanelHeaderProps {
+ menuItems: PaneMenuItem[]
+}
export const DocumentPanelHeader = memo(
forwardRef(function DocumentPanelHeader(
_props: DocumentPanelHeaderProps,
ref: React.ForwardedRef,
) {
+ const {menuItems} = _props
const {
onMenuAction,
onPaneClose,
onPaneSplit,
- menuItems,
menuItemGroups,
schemaType,
timelineStore,
@@ -40,11 +43,13 @@ export const DocumentPanelHeader = memo(
const {features} = useDeskTool()
const {index, BackLink, hasGroupSiblings} = usePaneRouter()
const {actions: fieldActions} = useFieldActions()
+
const menuNodes = useMemo(
() =>
resolveMenuNodes({actionHandler: onMenuAction, fieldActions, menuItems, menuItemGroups}),
[onMenuAction, fieldActions, menuItemGroups, menuItems],
)
+
const menuButtonNodes = useMemo(() => menuNodes.filter(isMenuNodeButton), [menuNodes])
const contextMenuNodes = useMemo(() => menuNodes.filter(isNotMenuNodeButton), [menuNodes])
const showTabs = views.length > 1
diff --git a/test/e2e/tests/components-api/document.spec.ts b/test/e2e/tests/components-api/document.spec.ts
new file mode 100644
index 00000000000..81e0704c5c8
--- /dev/null
+++ b/test/e2e/tests/components-api/document.spec.ts
@@ -0,0 +1,17 @@
+import {test, expect} from '@playwright/test'
+
+// We just need an id in the URL to render the form
+const id = 'test-id'
+
+test.describe('Document Components API:', () => {
+ test('document.components.layout', async ({page}) => {
+ page.goto(`/test/content/v3;formComponentsApi;${id}`)
+
+ await expect(
+ page
+ .getByTestId('child-parent-config-document-layout')
+ .getByTestId('parent-config-document-layout')
+ .getByTestId('document-pane'),
+ ).toBeVisible()
+ })
+})
diff --git a/test/e2e/tests/components-api/form.spec.ts b/test/e2e/tests/components-api/form.spec.ts
new file mode 100644
index 00000000000..5675beaf650
--- /dev/null
+++ b/test/e2e/tests/components-api/form.spec.ts
@@ -0,0 +1,28 @@
+import {test, expect} from '@playwright/test'
+
+// We just need an id in the URL to render the form
+const id = 'test-id'
+
+test.describe('Form Components API:', () => {
+ test('form.components.input', async ({page}) => {
+ page.goto(`/test/content/v3;formComponentsApi;${id}`)
+
+ await expect(
+ page
+ .getByTestId('child-parent-config-form-input')
+ .getByTestId('parent-config-form-input')
+ .getByTestId('string-input'),
+ ).toBeVisible()
+ })
+
+ test('form.components.field', async ({page}) => {
+ page.goto(`/test/content/v3;formComponentsApi;${id}`)
+
+ await expect(
+ page
+ .getByTestId('child-parent-config-form-field')
+ .getByTestId('parent-config-form-field')
+ .getByTestId('field-string'),
+ ).toBeVisible()
+ })
+})
diff --git a/test/e2e/tests/components-api/studio.spec.ts b/test/e2e/tests/components-api/studio.spec.ts
new file mode 100644
index 00000000000..f5052e5386b
--- /dev/null
+++ b/test/e2e/tests/components-api/studio.spec.ts
@@ -0,0 +1,43 @@
+import {test, expect} from '@playwright/test'
+
+test.describe('Studio Components API:', () => {
+ test('studio.components.layout', async ({page}) => {
+ page.goto('/test/content')
+ await expect(
+ page
+ .getByTestId('child-parent-config-studio-layout')
+ .getByTestId('parent-config-studio-layout')
+ .getByTestId('studio-layout'),
+ ).toBeVisible()
+ })
+
+ test('studio.components.navbar', async ({page}) => {
+ page.goto('/test/content')
+ await expect(
+ page
+ .getByTestId('child-parent-config-studio-navbar')
+ .getByTestId('parent-config-studio-navbar')
+ .getByTestId('studio-navbar'),
+ ).toBeVisible()
+ })
+
+ test('studio.components.logo', async ({page}) => {
+ page.goto('/test/content')
+ await expect(
+ page
+ .getByTestId('child-parent-config-studio-logo')
+ .getByTestId('parent-config-studio-logo')
+ .getByTestId('studio-logo'),
+ ).toBeVisible()
+ })
+
+ test('studio.components.toolMenu', async ({page}) => {
+ page.goto('/test/content')
+ await expect(
+ page
+ .getByTestId('child-parent-config-studio-tool-menu')
+ .getByTestId('parent-config-studio-tool-menu')
+ .getByTestId('tool-collapse-menu'),
+ ).toBeVisible()
+ })
+})
diff --git a/test/e2e/tests/default-layout/navbar.spec.ts b/test/e2e/tests/default-layout/navbar.spec.ts
index 547e5bbca8d..a2e72b55c24 100644
--- a/test/e2e/tests/default-layout/navbar.spec.ts
+++ b/test/e2e/tests/default-layout/navbar.spec.ts
@@ -14,4 +14,8 @@ test.describe('@sanity/default-layout: Navbar', () => {
state: 'visible',
})
})
+
+ test('render ActionModal on top of pane headers in desk tool', async ({page}) => {
+ await expect(page.locator('data-testid=studio-navbar')).toBeVisible()
+ })
})
diff --git a/test/e2e/tests/navbar/createNewDocumentNav.spec.ts b/test/e2e/tests/navbar/createNewDocumentNav.spec.ts
index 0bf704d54c0..99765819889 100644
--- a/test/e2e/tests/navbar/createNewDocumentNav.spec.ts
+++ b/test/e2e/tests/navbar/createNewDocumentNav.spec.ts
@@ -3,7 +3,7 @@ import {test} from '@sanity/test'
test('create new document from menu button', async ({page, baseURL}) => {
await page.goto(baseURL ?? '/test/content')
- await page.getByLabel('Create new document').click()
+ await page.getByTestId('new-document-button').click()
await page.getByTestId('new-document-button-search-input').fill('Author')
const authorLink = await page.getByRole('link', {name: 'Author', exact: true})