diff --git a/packages/ui/src/elements/CodeEditor/CodeEditor.tsx b/packages/ui/src/elements/CodeEditor/CodeEditor.tsx index 825dda5a4..036834ddc 100644 --- a/packages/ui/src/elements/CodeEditor/CodeEditor.tsx +++ b/packages/ui/src/elements/CodeEditor/CodeEditor.tsx @@ -15,6 +15,9 @@ const baseClass = 'code-editor' const CodeEditor: React.FC = (props) => { const { className, maxHeight, minHeight, options, readOnly, ...rest } = props const MIN_HEIGHT = minHeight ?? 56 // equivalent to 3 lines + + // Extract per-model settings to avoid global conflicts + const { insertSpaces, tabSize, trimAutoWhitespace, ...editorOptions } = options || {} const paddingFromProps = options?.padding ? (options.padding.top || 0) + (options.padding?.bottom || 0) : 0 @@ -36,8 +39,9 @@ const CodeEditor: React.FC = (props) => { className={classes} loading={} options={{ - detectIndentation: true, + detectIndentation: false, // use the tabSize on the model, set onMount hideCursorInOverviewRuler: true, + insertSpaces: false, minimap: { enabled: false, }, @@ -47,9 +51,9 @@ const CodeEditor: React.FC = (props) => { alwaysConsumeMouseWheel: false, }, scrollBeyondLastLine: false, - tabSize: 2, + trimAutoWhitespace: false, wordWrap: 'on', - ...options, + ...editorOptions, }} theme={theme === 'dark' ? 'vs-dark' : 'vs'} {...rest} @@ -64,6 +68,17 @@ const CodeEditor: React.FC = (props) => { }} onMount={(editor, monaco) => { rest.onMount?.(editor, monaco) + + // Set per-model options to avoid global conflicts + const model = editor.getModel() + if (model) { + model.updateOptions({ + insertSpaces: insertSpaces ?? true, + tabSize: tabSize ?? 4, + trimAutoWhitespace: trimAutoWhitespace ?? true, + }) + } + setDynamicHeight( Math.max(MIN_HEIGHT, editor.getValue().split('\n').length * 18 + 2 + paddingFromProps), ) diff --git a/packages/ui/src/fields/Code/index.tsx b/packages/ui/src/fields/Code/index.tsx index 143df342e..51b67eac7 100644 --- a/packages/ui/src/fields/Code/index.tsx +++ b/packages/ui/src/fields/Code/index.tsx @@ -1,7 +1,7 @@ 'use client' import type { CodeFieldClientComponent } from 'payload' -import React, { useCallback, useMemo } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import { CodeEditor } from '../../elements/CodeEditor/index.js' import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js' @@ -36,6 +36,9 @@ const CodeFieldComponent: CodeFieldClientComponent = (props) => { validate, } = props + const inputChangeFromRef = React.useRef<'system' | 'user'>('system') + const [editorKey, setEditorKey] = useState('') + const memoizedValidate = useCallback( (value, options) => { if (typeof validate === 'function') { @@ -48,15 +51,47 @@ const CodeFieldComponent: CodeFieldClientComponent = (props) => { const { customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {}, disabled, + initialValue, path, setValue, showError, value, - } = useField({ + } = useField({ potentiallyStalePath: pathFromProps, validate: memoizedValidate, }) + const [initialStringValue, setInitialStringValue] = useState(() => + (value || initialValue) !== undefined ? (value ?? initialValue) : undefined, + ) + + const handleChange = useCallback( + (val) => { + if (readOnly || disabled) { + return + } + inputChangeFromRef.current = 'user' + + try { + setValue(val ? val : null) + } catch (e) { + setValue(val ? val : null) + } + }, + [readOnly, disabled, setValue], + ) + + useEffect(() => { + if (inputChangeFromRef.current === 'system') { + setInitialStringValue( + (value || initialValue) !== undefined ? (value ?? initialValue) : undefined, + ) + setEditorKey(`${path}-${new Date().toString()}`) + } + + inputChangeFromRef.current = 'system' + }, [initialValue, path, value]) + const styles = useMemo(() => mergeFieldStyles(field), [field]) return ( @@ -86,11 +121,15 @@ const CodeFieldComponent: CodeFieldClientComponent = (props) => { {BeforeInput} null : (val) => setValue(val)} + key={editorKey} + onChange={handleChange} onMount={onMount} options={editorOptions} readOnly={readOnly || disabled} - value={(value as string) || ''} + value={initialStringValue} + wrapperProps={{ + id: `field-${path?.replace(/\./g, '__')}`, + }} /> {AfterInput} diff --git a/packages/ui/src/fields/JSON/index.tsx b/packages/ui/src/fields/JSON/index.tsx index c0b80a670..7c49f5ea4 100644 --- a/packages/ui/src/fields/JSON/index.tsx +++ b/packages/ui/src/fields/JSON/index.tsx @@ -33,6 +33,8 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => { validate, } = props + const { tabSize = 2 } = editorOptions || {} + const [jsonError, setJsonError] = useState() const inputChangeFromRef = React.useRef<'system' | 'user'>('system') const [editorKey, setEditorKey] = useState('') @@ -61,7 +63,7 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => { const [initialStringValue, setInitialStringValue] = useState(() => (value || initialValue) !== undefined - ? JSON.stringify(value ?? initialValue, null, 2) + ? JSON.stringify(value ?? initialValue, null, tabSize) : undefined, ) @@ -86,10 +88,14 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => { : `${uri}?${crypto.randomUUID ? crypto.randomUUID() : uuidv4()}` editor.setModel( - monaco.editor.createModel(JSON.stringify(value, null, 2), 'json', monaco.Uri.parse(newUri)), + monaco.editor.createModel( + JSON.stringify(value, null, tabSize), + 'json', + monaco.Uri.parse(newUri), + ), ) }, - [jsonSchema, value], + [jsonSchema, tabSize, value], ) const handleChange = useCallback( @@ -114,14 +120,14 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => { if (inputChangeFromRef.current === 'system') { setInitialStringValue( (value || initialValue) !== undefined - ? JSON.stringify(value ?? initialValue, null, 2) + ? JSON.stringify(value ?? initialValue, null, tabSize) : undefined, ) - setEditorKey(new Date().toString()) + setEditorKey(`${path}-${new Date().toString()}`) } inputChangeFromRef.current = 'system' - }, [initialValue, value]) + }, [initialValue, path, tabSize, value]) const styles = useMemo(() => mergeFieldStyles(field), [field]) diff --git a/test/_community/payload-types.ts b/test/_community/payload-types.ts index 6d7c96401..599c9dec1 100644 --- a/test/_community/payload-types.ts +++ b/test/_community/payload-types.ts @@ -84,7 +84,7 @@ export interface Config { 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; }; db: { - defaultIDType: number; + defaultIDType: string; }; globals: { menu: Menu; @@ -124,7 +124,7 @@ export interface UserAuthOperations { * via the `definition` "posts". */ export interface Post { - id: number; + id: string; title?: string | null; content?: { root: { @@ -149,7 +149,7 @@ export interface Post { * via the `definition` "media". */ export interface Media { - id: number; + id: string; updatedAt: string; createdAt: string; url?: string | null; @@ -193,7 +193,7 @@ export interface Media { * via the `definition` "users". */ export interface User { - id: number; + id: string; updatedAt: string; createdAt: string; email: string; @@ -217,24 +217,24 @@ export interface User { * via the `definition` "payload-locked-documents". */ export interface PayloadLockedDocument { - id: number; + id: string; document?: | ({ relationTo: 'posts'; - value: number | Post; + value: string | Post; } | null) | ({ relationTo: 'media'; - value: number | Media; + value: string | Media; } | null) | ({ relationTo: 'users'; - value: number | User; + value: string | User; } | null); globalSlug?: string | null; user: { relationTo: 'users'; - value: number | User; + value: string | User; }; updatedAt: string; createdAt: string; @@ -244,10 +244,10 @@ export interface PayloadLockedDocument { * via the `definition` "payload-preferences". */ export interface PayloadPreference { - id: number; + id: string; user: { relationTo: 'users'; - value: number | User; + value: string | User; }; key?: string | null; value?: @@ -267,7 +267,7 @@ export interface PayloadPreference { * via the `definition` "payload-migrations". */ export interface PayloadMigration { - id: number; + id: string; name?: string | null; batch?: number | null; updatedAt: string; @@ -393,7 +393,7 @@ export interface PayloadMigrationsSelect { * via the `definition` "menu". */ export interface Menu { - id: number; + id: string; globalText?: string | null; updatedAt?: string | null; createdAt?: string | null;