diff --git a/package.json b/package.json index 90b1b6822ee..162e8f0ae74 100644 --- a/package.json +++ b/package.json @@ -99,9 +99,6 @@ }, "prettier": "@sanity/prettier-config", "devDependencies": { - "@babel/preset-env": "^7.24.7", - "@babel/preset-react": "^7.24.7", - "@babel/preset-typescript": "^7.24.7", "@google-cloud/storage": "^7.11.0", "@playwright/test": "1.44.1", "@repo/dev-aliases": "workspace:*", diff --git a/packages/@repo/test-config/.depcheckrc.json b/packages/@repo/test-config/.depcheckrc.json new file mode 100644 index 00000000000..6f9da0ba787 --- /dev/null +++ b/packages/@repo/test-config/.depcheckrc.json @@ -0,0 +1,8 @@ +{ + "ignores": [ + "@babel/preset-env", + "@babel/preset-react", + "@babel/preset-typescript", + "babel-plugin-transform-vite-meta-hot" + ] +} diff --git a/packages/@repo/test-config/jest/createJestConfig.mjs b/packages/@repo/test-config/jest/createJestConfig.mjs index 174738b1f3d..8d1f3fbbd6a 100644 --- a/packages/@repo/test-config/jest/createJestConfig.mjs +++ b/packages/@repo/test-config/jest/createJestConfig.mjs @@ -1,9 +1,10 @@ /* eslint-disable tsdoc/syntax */ -import devAliases from '@repo/dev-aliases' - import path from 'node:path' + +import devAliases from '@repo/dev-aliases' import {escapeRegExp, omit} from 'lodash-es' + import {resolveDirName} from './resolveDirName.mjs' const dirname = resolveDirName(import.meta.url) @@ -71,7 +72,6 @@ export function createJestConfig(config = {}) { resolver: path.resolve(dirname, './resolver.cjs'), testEnvironment: path.resolve(dirname, './jsdom.jest.env.ts'), setupFiles: [...setupFiles, path.resolve(dirname, './setup.ts')], - // testEnvironment: 'jsdom', testEnvironmentOptions: { url: 'http://localhost:3333', }, @@ -103,6 +103,7 @@ export function createJestConfig(config = {}) { '@babel/preset-typescript', ['@babel/preset-react', {runtime: 'automatic'}], ], + plugins: ['babel-plugin-transform-vite-meta-hot'], }, ], }, diff --git a/packages/@repo/test-config/package.json b/packages/@repo/test-config/package.json index 8c8c09da1e3..536754f7f34 100644 --- a/packages/@repo/test-config/package.json +++ b/packages/@repo/test-config/package.json @@ -9,8 +9,12 @@ "./vitest": "./vitest/index.mjs" }, "devDependencies": { + "@babel/preset-env": "^7.24.7", + "@babel/preset-react": "^7.24.7", + "@babel/preset-typescript": "^7.24.7", "@jest/globals": "^29.7.0", "@repo/dev-aliases": "workspace:*", + "babel-plugin-transform-vite-meta-hot": "^1.0.0", "dotenv": "^16.4.5", "jest-environment-jsdom": "^29.7.0", "lodash-es": "^4.17.21", diff --git a/packages/@sanity/block-tools/src/HtmlDeserializer/preprocessors/word.ts b/packages/@sanity/block-tools/src/HtmlDeserializer/preprocessors/word.ts index 8d67841d195..5fb3e4cba60 100644 --- a/packages/@sanity/block-tools/src/HtmlDeserializer/preprocessors/word.ts +++ b/packages/@sanity/block-tools/src/HtmlDeserializer/preprocessors/word.ts @@ -70,16 +70,16 @@ export default (html: string, doc: Document): Document => { for (let i = mappedElements.snapshotLength - 1; i >= 0; i--) { const mappedElm = mappedElements.snapshotItem(i) as HTMLElement const tags = elementMap[mappedElm.className] - const text = new Text(mappedElm.textContent || '') + const text = doc.createTextNode(mappedElm.textContent || '') if (!tags) { continue } - const parentElement = document.createElement(tags[0]) + const parentElement = doc.createElement(tags[0]) let parent = parentElement let child = parentElement tags.slice(1).forEach((tag) => { - child = document.createElement(tag) + child = doc.createElement(tag) parent.appendChild(child) parent = child }) diff --git a/packages/sanity/src/core/error/ErrorLogger.tsx b/packages/sanity/src/core/error/ErrorLogger.tsx index 708699b8baf..87dbfb75a50 100644 --- a/packages/sanity/src/core/error/ErrorLogger.tsx +++ b/packages/sanity/src/core/error/ErrorLogger.tsx @@ -64,5 +64,9 @@ function isKnownError(err: Error): boolean { return true } + if ('ViteDevServerStoppedError' in err && err.ViteDevServerStoppedError) { + return true + } + return false } diff --git a/packages/sanity/src/core/form/inputs/files/ImageInput/ImageInput.tsx b/packages/sanity/src/core/form/inputs/files/ImageInput/ImageInput.tsx index b7a6b264319..d9f85e56a36 100644 --- a/packages/sanity/src/core/form/inputs/files/ImageInput/ImageInput.tsx +++ b/packages/sanity/src/core/form/inputs/files/ImageInput/ImageInput.tsx @@ -67,28 +67,6 @@ function BaseImageInputComponent(props: BaseImageInputProps): JSX.Element { const uploadSubscription = useRef(null) - /** - * The upload progress state wants to use the same height as any previous image - * to avoid layout shifts and jumps - */ - const previewElementRef = useRef<{el: HTMLDivElement | null; height: number}>({ - el: null, - height: 0, - }) - const setPreviewElementHeight = useCallback((node: HTMLDivElement | null) => { - if (node) { - previewElementRef.current.el = node - previewElementRef.current.height = node.offsetHeight - } else { - /** - * If `node` is `null` then it means the `FileTarget` in `ImageInputAsset` is being unmounted and we want to - * capture its height before it's removed from the DOM. - */ - - previewElementRef.current.height = previewElementRef.current.el?.offsetHeight || 0 - previewElementRef.current.el = null - } - }, []) const getFileTone = useCallback(() => { const acceptedFiles = hoveringFiles.filter((file) => resolveUploader(schemaType, file)) const rejectedFilesCount = hoveringFiles.length - acceptedFiles.length @@ -201,9 +179,6 @@ function BaseImageInputComponent(props: BaseImageInputProps): JSX.Element { const handleClearField = useCallback(() => { onChange([unset(['asset']), unset(['crop']), unset(['hotspot'])]) - - previewElementRef.current.el = null - previewElementRef.current.height = 0 }, [onChange]) const handleRemoveButtonClick = useCallback(() => { // When removing the image, we should also remove any crop and hotspot @@ -224,9 +199,6 @@ function BaseImageInputComponent(props: BaseImageInputProps): JSX.Element { .map((key) => unset([key])) onChange(isEmpty && !valueIsArrayElement() ? unset() : removeKeys) - - previewElementRef.current.el = null - previewElementRef.current.height = 0 }, [onChange, value, valueIsArrayElement]) const handleOpenDialog = useCallback(() => { onPathFocus(['hotspot']) @@ -303,15 +275,16 @@ function BaseImageInputComponent(props: BaseImageInputProps): JSX.Element { menuButtonElement?.focus() }, [menuButtonElement]) - const renderPreview = useCallback(() => { + const renderPreview = useCallback<() => JSX.Element>(() => { + if (!value) { + return <> + } return ( ) }, @@ -420,7 +391,6 @@ function BaseImageInputComponent(props: BaseImageInputProps): JSX.Element { // eslint-disable-next-line react/display-name return (inputProps: Omit) => ( ) }, [ @@ -449,13 +420,13 @@ function BaseImageInputComponent(props: BaseImageInputProps): JSX.Element { handleFilesOver, handleSelectFiles, hoveringFiles, + imageUrlBuilder, isStale, readOnly, renderAssetMenu, renderPreview, renderUploadPlaceholder, renderUploadState, - setPreviewElementHeight, value, ]) const renderHotspotInput = useCallback( diff --git a/packages/sanity/src/core/form/inputs/files/ImageInput/ImageInputAsset.tsx b/packages/sanity/src/core/form/inputs/files/ImageInput/ImageInputAsset.tsx index d59ce2384b8..c07bbfdf903 100644 --- a/packages/sanity/src/core/form/inputs/files/ImageInput/ImageInputAsset.tsx +++ b/packages/sanity/src/core/form/inputs/files/ImageInput/ImageInputAsset.tsx @@ -1,36 +1,36 @@ import {type UploadState} from '@sanity/types' import {Box, type CardTone} from '@sanity/ui' -import {type FocusEvent, forwardRef, memo, useMemo} from 'react' +import {type FocusEvent, memo, useMemo} from 'react' import {ChangeIndicator} from '../../../../changeIndicators' import {type InputProps} from '../../../types' import {FileTarget} from '../common/styles' import {UploadWarning} from '../common/UploadWarning' +import {type ImageUrlBuilder} from '../types' import {type BaseImageInputProps, type BaseImageInputValue, type FileInfo} from './types' +import {usePreviewImageSource} from './usePreviewImageSource' const ASSET_FIELD_PATH = ['asset'] as const -function ImageInputAssetComponent( - props: { - elementProps: BaseImageInputProps['elementProps'] - handleClearUploadState: () => void - handleFilesOut: () => void - handleFilesOver: (hoveringFiles: FileInfo[]) => void - handleFileTargetFocus: (event: FocusEvent) => void - handleSelectFiles: (files: File[]) => void - hoveringFiles: FileInfo[] - inputProps: Omit - isStale: boolean - readOnly: boolean | undefined - renderAssetMenu(): JSX.Element | null - renderPreview: () => JSX.Element - renderUploadPlaceholder(): JSX.Element - renderUploadState(uploadState: UploadState): JSX.Element - tone: CardTone - value: BaseImageInputValue | undefined - }, - forwardedRef: React.ForwardedRef, -) { +function ImageInputAssetComponent(props: { + elementProps: BaseImageInputProps['elementProps'] + handleClearUploadState: () => void + handleFilesOut: () => void + handleFilesOver: (hoveringFiles: FileInfo[]) => void + handleFileTargetFocus: (event: FocusEvent) => void + handleSelectFiles: (files: File[]) => void + hoveringFiles: FileInfo[] + imageUrlBuilder: ImageUrlBuilder + inputProps: Omit + isStale: boolean + readOnly: boolean | undefined + renderAssetMenu(): JSX.Element | null + renderPreview: () => JSX.Element + renderUploadPlaceholder(): JSX.Element + renderUploadState(uploadState: UploadState): JSX.Element + tone: CardTone + value: BaseImageInputValue | undefined +}) { const { elementProps, handleClearUploadState, @@ -48,13 +48,15 @@ function ImageInputAssetComponent( renderUploadState, tone, value, + imageUrlBuilder, } = props const hasValueOrUpload = Boolean(value?._upload || value?.asset) const path = useMemo(() => inputProps.path.concat(ASSET_FIELD_PATH), [inputProps.path]) + const {customProperties} = usePreviewImageSource({value, imageUrlBuilder}) return ( - <> +
{isStale && ( @@ -79,7 +81,7 @@ function ImageInputAssetComponent( > {!value?.asset && renderUploadPlaceholder()} {!value?._upload && value?.asset && ( -
+
{renderPreview()} {renderAssetMenu()}
@@ -87,7 +89,7 @@ function ImageInputAssetComponent( )} - +
) } -export const ImageInputAsset = memo(forwardRef(ImageInputAssetComponent)) +export const ImageInputAsset = memo(ImageInputAssetComponent) diff --git a/packages/sanity/src/core/form/inputs/files/ImageInput/ImageInputPreview.tsx b/packages/sanity/src/core/form/inputs/files/ImageInput/ImageInputPreview.tsx index 99497c91700..ab092ae5d1a 100644 --- a/packages/sanity/src/core/form/inputs/files/ImageInput/ImageInputPreview.tsx +++ b/packages/sanity/src/core/form/inputs/files/ImageInput/ImageInputPreview.tsx @@ -1,49 +1,40 @@ -import {isImageSource} from '@sanity/asset-utils' import {type ImageSchemaType} from '@sanity/types' import {memo, useMemo} from 'react' -import {useDevicePixelRatio} from 'use-device-pixel-ratio' import {useTranslation} from '../../../../i18n' import {type UploaderResolver} from '../../../studio/uploads/types' import {type ImageUrlBuilder} from '../types' import {ImagePreview} from './ImagePreview' import {type BaseImageInputValue, type FileInfo} from './types' +import {usePreviewImageSource} from './usePreviewImageSource' export const ImageInputPreview = memo(function ImageInputPreviewComponent(props: { directUploads: boolean | undefined handleOpenDialog: () => void hoveringFiles: FileInfo[] imageUrlBuilder: ImageUrlBuilder - initialHeight: number | undefined readOnly: boolean | undefined resolveUploader: UploaderResolver schemaType: ImageSchemaType - value: BaseImageInputValue | undefined + value: BaseImageInputValue }) { const { directUploads, handleOpenDialog, hoveringFiles, imageUrlBuilder, - initialHeight, readOnly, resolveUploader, schemaType, value, } = props - const isValueImageSource = useMemo(() => isImageSource(value), [value]) - if (!value || !isValueImageSource) { - return null - } - return ( void hoveringFiles: FileInfo[] imageUrlBuilder: ImageUrlBuilder - initialHeight: number | undefined readOnly: boolean | undefined resolveUploader: UploaderResolver schemaType: ImageSchemaType @@ -68,7 +58,6 @@ function RenderImageInputPreview(props: { handleOpenDialog, hoveringFiles, imageUrlBuilder, - initialHeight, readOnly, resolveUploader, schemaType, @@ -84,20 +73,17 @@ function RenderImageInputPreview(props: { () => hoveringFiles.length - acceptedFiles.length, [acceptedFiles, hoveringFiles], ) - const dpr = useDevicePixelRatio() - const imageUrl = useMemo( - () => imageUrlBuilder.width(2000).fit('max').image(value).dpr(dpr).auto('format').url(), - [dpr, imageUrlBuilder, value], - ) + + const {url} = usePreviewImageSource({value, imageUrlBuilder}) + return ( 0} - initialHeight={initialHeight} isRejected={rejectedFilesCount > 0 || !directUploads} onDoubleClick={handleOpenDialog} readOnly={readOnly} - src={imageUrl} + src={url} /> ) } diff --git a/packages/sanity/src/core/form/inputs/files/ImageInput/ImagePreview.styled.tsx b/packages/sanity/src/core/form/inputs/files/ImageInput/ImagePreview.styled.tsx index 7b54614372e..630ecaa030d 100644 --- a/packages/sanity/src/core/form/inputs/files/ImageInput/ImagePreview.styled.tsx +++ b/packages/sanity/src/core/form/inputs/files/ImageInput/ImagePreview.styled.tsx @@ -1,38 +1,29 @@ import {Card, type CardTone, Flex, rgba, studioTheme} from '@sanity/ui' +import {useColorSchemeValue} from 'sanity' import {css, styled} from 'styled-components' -export const MAX_DEFAULT_HEIGHT = 30 - export const RatioBox = styled(Card)` position: relative; width: 100%; - overflow: hidden; - overflow: clip; min-height: 3.75rem; - max-height: 20rem; + max-height: min(calc(var(--image-height) * 1px), 30vh); + aspect-ratio: var(--image-width) / var(--image-height); - & > div[data-container] { - top: 0; - left: 0; + & img { + display: block; width: 100%; height: 100%; - display: flex !important; - align-items: center; - justify-content: center; - } - - & img { - max-width: 100%; - max-height: 100%; + object-fit: scale-down; + object-position: center; } ` export const Overlay = styled(Flex)<{ - $drag: boolean $tone: Exclude -}>(({$drag, $tone}) => { - const textColor = studioTheme.color.light[$tone].card.enabled.fg - const backgroundColor = rgba(studioTheme.color.light[$tone].card.enabled.bg, 0.8) +}>(({$tone}) => { + const colorScheme = useColorSchemeValue() + const textColor = studioTheme.color[colorScheme][$tone].card.enabled.fg + const backgroundColor = rgba(studioTheme.color[colorScheme][$tone].card.enabled.bg, 0.8) return css` position: absolute; @@ -40,9 +31,9 @@ export const Overlay = styled(Flex)<{ left: 0; right: 0; bottom: 0; - backdrop-filter: ${$drag ? 'blur(10px)' : ''}; + backdrop-filter: blur(10px); color: ${$tone ? textColor : ''}; - background-color: ${$drag ? backgroundColor : 'transparent'}; + background-color: ${backgroundColor}; ` }) diff --git a/packages/sanity/src/core/form/inputs/files/ImageInput/ImagePreview.tsx b/packages/sanity/src/core/form/inputs/files/ImageInput/ImagePreview.tsx index 662b340704b..b1a5070453d 100644 --- a/packages/sanity/src/core/form/inputs/files/ImageInput/ImagePreview.tsx +++ b/packages/sanity/src/core/form/inputs/files/ImageInput/ImagePreview.tsx @@ -1,59 +1,25 @@ import {AccessDeniedIcon, ImageIcon, ReadOnlyIcon} from '@sanity/icons' -import {Box, Card, type CardTone, Heading, Text, useElementRect} from '@sanity/ui' +import {Box, type Card, type CardTone, Heading, Text} from '@sanity/ui' import {type ComponentProps, type ReactNode, useCallback, useEffect, useState} from 'react' import {LoadingBlock} from '../../../../components/loadingBlock' import {useTranslation} from '../../../../i18n' -import {FlexOverlay, MAX_DEFAULT_HEIGHT, Overlay, RatioBox} from './ImagePreview.styled' +import {FlexOverlay, Overlay, RatioBox} from './ImagePreview.styled' interface Props { alt: string drag: boolean - initialHeight: number | undefined isRejected: boolean readOnly?: boolean | null src: string } -/* - Used for setting the initial image height - specifically for images - that are small and so can take less space in the document -*/ -const getImageSize = (src: string): number[] => { - const imageUrlParams = new URLSearchParams(src.split('?')[1]) - const rect = imageUrlParams.get('rect') - - if (rect) { - return [rect.split(',')[2], rect.split(',')[3]].map(Number) - } - - return src.split('-')[1].split('.')[0].split('x').map(Number) -} - export function ImagePreview(props: ComponentProps & Props) { - const {drag, readOnly, isRejected, src, initialHeight, ...rest} = props + const {drag, readOnly, isRejected, src, ...rest} = props const [isLoaded, setLoaded] = useState(false) - const [rootElement, setRootElement] = useState(null) - const rootRect = useElementRect(rootElement) - const rootWidth = rootRect?.width || 0 const acceptTone = isRejected || readOnly ? 'critical' : 'primary' const tone = drag ? acceptTone : 'default' - const maxHeightToPx = (MAX_DEFAULT_HEIGHT * document.documentElement.clientHeight) / 100 // convert from vh to px, max height of the input - - const [imageWidth, imageHeight] = getImageSize(src) - - const imageRatio = imageWidth / imageHeight - - // is the image wider than root? if so calculate the resized height - const renderedImageHeight = imageWidth > rootWidth ? rootWidth / imageRatio : imageHeight - - /* - if the rendered image is smaller than the max height then it doesn't require a height set - otherwise, set the max height (to prevent a large image in the document) - */ - const rootHeight = renderedImageHeight < maxHeightToPx ? null : `${MAX_DEFAULT_HEIGHT}vh` - useEffect(() => { /* set for when the src is being switched when the image input already had a image src - meaning it already had an asset */ @@ -65,30 +31,20 @@ export function ImagePreview(props: ComponentProps & Props) { }, []) const {t} = useTranslation() - return ( - - - {!isLoaded && ( - } /> - )} - {props.alt} - + return ( + + {!isLoaded && } />} + {props.alt} {drag && ( @@ -132,15 +88,13 @@ function getHoverTextTranslationKey({ function OverlayComponent({ cardTone, - drag, content, }: { cardTone: Exclude - drag: boolean content: ReactNode }) { return ( - + {content} diff --git a/packages/sanity/src/core/form/inputs/files/ImageInput/usePreviewImageSource.ts b/packages/sanity/src/core/form/inputs/files/ImageInput/usePreviewImageSource.ts new file mode 100644 index 00000000000..97d532802ca --- /dev/null +++ b/packages/sanity/src/core/form/inputs/files/ImageInput/usePreviewImageSource.ts @@ -0,0 +1,55 @@ +import {getImageDimensions, isImageSource, type SanityImageDimensions} from '@sanity/asset-utils' +import {type CSSProperties, useMemo} from 'react' +import {useDevicePixelRatio} from 'use-device-pixel-ratio' + +import {type ImageUrlBuilder} from '../types' +import {type BaseImageInputValue} from './types' + +export function usePreviewImageSource({ + value, + imageUrlBuilder, +}: { + value: Value + imageUrlBuilder: ImageUrlBuilder +}): { + url: Value extends undefined ? undefined : string + dimensions: SanityImageDimensions + customProperties: CSSProperties +} { + const dpr = useDevicePixelRatio() + + const url = useMemo( + () => + value && isImageSource(value) + ? imageUrlBuilder.width(2000).fit('max').image(value).dpr(dpr).auto('format').url() + : undefined, + [dpr, imageUrlBuilder, value], + ) as Value extends undefined ? undefined : string + + const dimensions = useMemo( + () => + url + ? getImageDimensions(url) + : { + width: 0, + height: 0, + aspectRatio: 0, + }, + [url], + ) + + const customProperties = useMemo( + () => + ({ + '--image-width': dimensions.width, + '--image-height': dimensions.height, + }) as CSSProperties, + [dimensions.width, dimensions.height], + ) + + return { + url, + dimensions, + customProperties, + } +} diff --git a/packages/sanity/src/core/form/inputs/files/common/UploadProgress.styled.tsx b/packages/sanity/src/core/form/inputs/files/common/UploadProgress.styled.tsx index 7859aa59e4e..8d650e0a8bd 100644 --- a/packages/sanity/src/core/form/inputs/files/common/UploadProgress.styled.tsx +++ b/packages/sanity/src/core/form/inputs/files/common/UploadProgress.styled.tsx @@ -1,12 +1,14 @@ -import {Card, Code, Flex, Stack} from '@sanity/ui' +import {Code, Flex, Stack} from '@sanity/ui' import {styled} from 'styled-components' -export const CardWrapper = styled(Card)` - min-height: 82px; +import {RatioBox} from '../ImageInput/ImagePreview.styled' + +export const CardWrapper = styled(RatioBox)` box-sizing: border-box; ` export const FlexWrapper = styled(Flex)` + box-sizing: border-box; text-overflow: ellipsis; overflow: hidden; overflow: clip; diff --git a/packages/sanity/src/core/form/inputs/files/common/UploadProgress.tsx b/packages/sanity/src/core/form/inputs/files/common/UploadProgress.tsx index 63394ae5407..72cd27922b1 100644 --- a/packages/sanity/src/core/form/inputs/files/common/UploadProgress.tsx +++ b/packages/sanity/src/core/form/inputs/files/common/UploadProgress.tsx @@ -12,11 +12,10 @@ type Props = { uploadState: UploadState onCancel?: () => void onStale?: () => void - height?: number } const elapsedMs = (date: string): number => new Date().getTime() - new Date(date).getTime() -export function UploadProgress({uploadState, onCancel, onStale, height}: Props) { +export function UploadProgress({uploadState, onCancel, onStale}: Props) { const filename = uploadState.file.name useEffect(() => { @@ -27,13 +26,15 @@ export function UploadProgress({uploadState, onCancel, onStale, height}: Props) const {t} = useTranslation() return ( - - + + diff --git a/packages/sanity/src/core/form/studio/FormBuilder.tsx b/packages/sanity/src/core/form/studio/FormBuilder.tsx index 98a744aefbc..58e1f1d04bf 100644 --- a/packages/sanity/src/core/form/studio/FormBuilder.tsx +++ b/packages/sanity/src/core/form/studio/FormBuilder.tsx @@ -32,12 +32,7 @@ import { import {DocumentFieldActionsProvider} from './contexts/DocumentFieldActions' import {FormBuilderInputErrorBoundary} from './FormBuilderInputErrorBoundary' import {FormProvider} from './FormProvider' -import { - shouldArrayDialogOpen, - TreeEditingDialog, - TreeEditingEnabledProvider, - useTreeEditingEnabled, -} from './tree-editing' +import {TreeEditingDialog, TreeEditingEnabledProvider, useTreeEditingEnabled} from './tree-editing' /** * @alpha @@ -307,15 +302,9 @@ interface RootInputProps { function RootInput(props: RootInputProps) { const {rootInputProps, onPathOpen, openPath, renderInput} = props const treeEditing = useTreeEditingEnabled() - - const open = useMemo( - () => shouldArrayDialogOpen(rootInputProps.schemaType, openPath), - [openPath, rootInputProps.schemaType], - ) - const isRoot = rootInputProps.id === 'root' - const arrayEditingModal = treeEditing.enabled && isRoot && open && ( + const arrayEditingModal = treeEditing.enabled && isRoot && ( { - test('should return false if its in the base document (path [])', () => { - const objectSchemaType = { - jsonType: 'object', - fields: [], - name: '', - // eslint-disable-next-line camelcase - __experimental_search: [], - } as ObjectSchemaType - - const {result} = renderHook(() => shouldArrayDialogOpen(objectSchemaType, [])) - - expect(result.current).toEqual(false) - }) - - test('should return false if array has a reference', () => { - const referenceSchemaType = { - jsonType: 'object', - fields: [ - { - name: 'referenceAuthor', - type: { - type: 'array', - of: [{type: 'reference', to: [{type: 'author'}]}], - }, - }, - ], - name: '', - // eslint-disable-next-line camelcase - __experimental_search: [], - } as unknown as ObjectSchemaType - - const {result} = renderHook(() => - shouldArrayDialogOpen(referenceSchemaType, ['referenceAuthor']), - ) - - expect(result.current).toEqual(false) - }) - - test('should return false if array is PTE', () => { - const referenceSchemaType = { - jsonType: 'object', - fields: [ - { - name: 'pte', - type: { - type: 'array', - of: [{type: 'block'}], - }, - }, - ], - name: '', - // eslint-disable-next-line camelcase - __experimental_search: [], - } as unknown as ObjectSchemaType - - const {result} = renderHook(() => shouldArrayDialogOpen(referenceSchemaType, ['pte'])) - - expect(result.current).toEqual(false) - }) - - test('should return true if its an array of plain objects - not pte, an array of references or in base document', () => { - const objectSchemaType = { - jsonType: 'object', - fields: [ - { - type: 'array', - name: 'arrayOfObjects', - of: [ - { - type: 'object', - name: 'object1', - fields: [ - { - name: 'name', - type: 'string', - title: 'name', - }, - { - name: 'age', - type: 'number', - title: 'age', - }, - ], - }, - ], - }, - ], - name: '', - // eslint-disable-next-line camelcase - __experimental_search: [], - } as unknown as ObjectSchemaType - - const {result} = renderHook(() => shouldArrayDialogOpen(objectSchemaType, ['object1'])) - - expect(result.current).toEqual(false) - }) -}) diff --git a/packages/sanity/src/core/form/studio/tree-editing/index.ts b/packages/sanity/src/core/form/studio/tree-editing/index.ts index 634ba1a809b..3e91c5effbb 100644 --- a/packages/sanity/src/core/form/studio/tree-editing/index.ts +++ b/packages/sanity/src/core/form/studio/tree-editing/index.ts @@ -1,4 +1,3 @@ export * from './components' export * from './context' export * from './hooks' -export {shouldArrayDialogOpen} from './utils' diff --git a/packages/sanity/src/core/form/studio/tree-editing/utils/index.ts b/packages/sanity/src/core/form/studio/tree-editing/utils/index.ts index 0f1f8a87d0b..c2fe986cc77 100644 --- a/packages/sanity/src/core/form/studio/tree-editing/utils/index.ts +++ b/packages/sanity/src/core/form/studio/tree-editing/utils/index.ts @@ -1,4 +1,3 @@ export * from './build-tree-editing-state' export * from './findArrayTypePaths' export * from './getSchemaField' -export * from './shouldArrayDialogOpen' diff --git a/packages/sanity/src/core/form/studio/tree-editing/utils/shouldArrayDialogOpen.ts b/packages/sanity/src/core/form/studio/tree-editing/utils/shouldArrayDialogOpen.ts deleted file mode 100644 index 53050447cfa..00000000000 --- a/packages/sanity/src/core/form/studio/tree-editing/utils/shouldArrayDialogOpen.ts +++ /dev/null @@ -1,49 +0,0 @@ -import {toString} from '@sanity/util/paths' -import { - isArrayOfBlocksSchemaType, - isArrayOfObjectsSchemaType, - type ObjectSchemaType, - type Path, -} from 'sanity' - -// import {isPortableTextSchemaType} from './asserters' -import {getRootPath} from './getRootPath' -import {getSchemaField} from './getSchemaField' - -/** - * A utility function to check if the global array editing dialog should be open. - * @param schemaType - The schema object that we are opening - * @param path - The path that we are focusing on - * @returns Returns true if the dialog should be open - * @internal - */ -export function shouldArrayDialogOpen(schemaType: ObjectSchemaType, path: Path): boolean { - // If the path is empty, we can't determine if the array dialog is open - if (path.length === 0) return false - - const rootPath = getRootPath(path) - - // Get the field for the first segments - const field = getSchemaField(schemaType, toString(rootPath)) - - // Check if the field is an array of objects - if (isArrayOfObjectsSchemaType(field?.type)) { - // Check if the array of objects is an array of references. - const isArrayOfReferences = field.type.of.every((type) => type?.hasOwnProperty('to')) - const isPortableText = isArrayOfBlocksSchemaType(field.type) - - // Return false if the array of objects is an array of references - // since these are edited inline and not in a dialog. - if (isArrayOfReferences) return false - - // Return false if the array of objects is an array of portable text - // since these are edited inline and not in a dialog. - if (isPortableText) return false - - // Else, return true if it is an array of objects - return true - } - - // Otherwise, return false - return false -} diff --git a/packages/sanity/src/core/store/key-value/localStorageSWR.ts b/packages/sanity/src/core/store/key-value/localStorageSWR.ts index d4a4273d6ea..366e428e926 100644 --- a/packages/sanity/src/core/store/key-value/localStorageSWR.ts +++ b/packages/sanity/src/core/store/key-value/localStorageSWR.ts @@ -1,27 +1,16 @@ import {isEqual} from 'lodash' -import {fromEvent, merge, NEVER} from 'rxjs' -import {distinctUntilChanged, filter, map, tap} from 'rxjs/operators' +import {merge, of} from 'rxjs' +import {distinctUntilChanged, tap} from 'rxjs/operators' import {localStoreStorage} from './storage/localStoreStorage' import {type KeyValueStore, type KeyValueStoreValue} from './types' -// Whether or not to enable instant user sync between tabs -// if set to true, the setting will update instantly across all tabs -const ENABLE_CROSS_TAB_SYNC = false - /** * Wraps a KeyValueStore and adds Stale-While-Revalidate (SWR) behavior to it */ export function withLocalStorageSWR(wrappedStore: KeyValueStore): KeyValueStore { - const storageEvent = ENABLE_CROSS_TAB_SYNC ? fromEvent(window, 'storage') : NEVER - function getKey(key: string) { - const lsUpdates = storageEvent.pipe( - filter((event) => event.key === key), - map(() => localStoreStorage.getKey(key)), - ) - - return merge(lsUpdates, wrappedStore.getKey(key)).pipe( + return merge(of(localStoreStorage.getKey(key)), wrappedStore.getKey(key)).pipe( distinctUntilChanged(isEqual), tap((value) => { localStoreStorage.setKey(key, value) diff --git a/packages/sanity/src/core/studio/StudioErrorBoundary.tsx b/packages/sanity/src/core/studio/StudioErrorBoundary.tsx index 7c26dc8179f..32b0de92a00 100644 --- a/packages/sanity/src/core/studio/StudioErrorBoundary.tsx +++ b/packages/sanity/src/core/studio/StudioErrorBoundary.tsx @@ -11,7 +11,14 @@ import { Stack, Text, } from '@sanity/ui' -import {type ComponentType, type ErrorInfo, type ReactNode, useCallback, useState} from 'react' +import { + type ComponentType, + type ErrorInfo, + lazy, + type ReactNode, + useCallback, + useState, +} from 'react' import {ErrorActions, isDev, isProd} from 'sanity' import {styled} from 'styled-components' import {useHotModuleReload} from 'use-hot-module-reload' @@ -22,6 +29,17 @@ import {CorsOriginError} from '../store' import {isRecord} from '../util' import {CorsOriginErrorScreen, SchemaErrorsScreen} from './screens' +/** + * The DevServerStoppedErrorScreen will always have been lazy loaded to client + * in instances where it is used, since DevServerStoppedError is only thrown + * when this module is loaded, and this screen is also conditional on this error type + */ +const DevServerStoppedErrorScreen = lazy(() => + import('./ViteDevServerStopped').then((DevServerStopped) => ({ + default: DevServerStopped.DevServerStoppedErrorScreen, + })), +) + interface StudioErrorBoundaryProps { children: ReactNode heading?: string @@ -80,6 +98,10 @@ export const StudioErrorBoundary: ComponentType = ({ return } + if (error && 'ViteDevServerStoppedError' in error && error.ViteDevServerStoppedError) { + return + } + if (!error) { return {children} } diff --git a/packages/sanity/src/core/studio/StudioLayout.tsx b/packages/sanity/src/core/studio/StudioLayout.tsx index 5641104e904..9c992354bcc 100644 --- a/packages/sanity/src/core/studio/StudioLayout.tsx +++ b/packages/sanity/src/core/studio/StudioLayout.tsx @@ -1,7 +1,7 @@ /* eslint-disable i18next/no-literal-string, @sanity/i18n/no-attribute-template-literals */ import {Card, Flex} from '@sanity/ui' import {startCase} from 'lodash' -import {Suspense, useCallback, useEffect, useMemo, useState} from 'react' +import {lazy, Suspense, useCallback, useEffect, useMemo, useState} from 'react' import {NavbarContext} from 'sanity/_singletons' import {RouteScope, useRouter, useRouterState} from 'sanity/router' import {styled} from 'styled-components' @@ -18,6 +18,14 @@ import { import {StudioErrorBoundary} from './StudioErrorBoundary' import {useWorkspace} from './workspace' +const DetectViteDevServerStopped = lazy(() => + import('./ViteDevServerStopped').then((DevServerStopped) => ({ + default: DevServerStopped.DetectViteDevServerStopped, + })), +) + +const detectViteDevServerStopped = import.meta.hot && process.env.NODE_ENV === 'development' + const SearchFullscreenPortalCard = styled(Card)` height: 100%; left: 0; @@ -173,6 +181,7 @@ export function StudioLayoutComponent() { {/* By using the tool name as the key on the error boundary, we force it to re-render when switching tools, which ensures we don't show the wrong tool having crashed */} + {detectViteDevServerStopped && }