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 && ( - - )} - - - )} - - - - - {inspectDialog} - -
- - - - - )} - - {showInspector && ( - - - - )} - - - + {!isDeleting && isDeleted && ( + + )} + + + )} + + + + + {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})