Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@
"templates/**/pnpm-lock.yaml": "pnpm runts scripts/remove-template-lock-files.ts",
"tsconfig.json": "node scripts/reset-tsconfig.js"
},
"dependencies": {
"json5": "2.2.3",
"yaml": "2.8.0"
},
"devDependencies": {
"@jest/globals": "29.7.0",
"@libsql/client": "0.14.0",
Expand Down
3 changes: 2 additions & 1 deletion packages/payload/src/fields/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1066,6 +1066,7 @@ export type JSONField = {
Label?: CustomComponent<JSONFieldLabelClientComponent | JSONFieldLabelServerComponent>
} & Admin['components']
editorOptions?: EditorProps['options']
format?: 'json' | 'json5' | 'yaml' // Add format option with three choices
maxHeight?: number
} & Admin

Expand All @@ -1079,7 +1080,7 @@ export type JSONField = {
} & Omit<FieldBase, 'admin' | 'validate'>

export type JSONFieldClient = {
admin?: AdminClient & Pick<JSONField['admin'], 'editorOptions' | 'maxHeight'>
admin?: AdminClient & Pick<JSONField['admin'], 'editorOptions' | 'format' | 'maxHeight'>
} & Omit<FieldBaseClient, 'admin'> &
Pick<JSONField, 'jsonSchema' | 'type'>

Expand Down
4 changes: 3 additions & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
"bson-objectid": "2.0.4",
"date-fns": "4.1.0",
"dequal": "2.0.3",
"json5": "^2.2.3",
"md5": "2.3.0",
"object-to-formdata": "4.5.1",
"qs-esm": "7.0.2",
Expand All @@ -137,7 +138,8 @@
"sonner": "^1.7.2",
"ts-essentials": "10.0.3",
"use-context-selector": "2.0.0",
"uuid": "10.0.0"
"uuid": "10.0.0",
"yaml": "^2.3.1"
},
"devDependencies": {
"@babel/cli": "7.26.4",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/exports/rsc/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { FieldDiffLabel } from '../../elements/FieldDiffLabel/index.js'
export { parseData, stringifyData } from '../../fields/JSON/server.js'
export { File } from '../../graphics/File/index.js'
export { CheckIcon } from '../../icons/Check/index.js'
export { copyDataFromLocaleHandler } from '../../utilities/copyDataFromLocale.js'
Expand Down
5 changes: 5 additions & 0 deletions packages/ui/src/fields/JSON/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use client'

import { parseData, stringifyData } from './server.js'

export { parseData, stringifyData }
86 changes: 47 additions & 39 deletions packages/ui/src/fields/JSON/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { FieldError } from '../FieldError/index.js'
import { FieldLabel } from '../FieldLabel/index.js'
import { mergeFieldStyles } from '../mergeFieldStyles.js'
import { fieldBaseClass } from '../shared/index.js'
import { parseData, stringifyData } from './client.js'
import './index.scss'

const baseClass = 'json-field'
Expand All @@ -22,7 +23,7 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
const {
field,
field: {
admin: { className, description, editorOptions, maxHeight } = {},
admin: { className, description, editorOptions, format = 'json', maxHeight } = {},
jsonSchema,
label,
localized,
Expand Down Expand Up @@ -59,69 +60,76 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
validate: memoizedValidate,
})

const [initialStringValue, setInitialStringValue] = useState<string | undefined>(() =>
(value || initialValue) !== undefined
? JSON.stringify(value ?? initialValue, null, 2)
: undefined,
)
const [initialStringValue, setInitialStringValue] = useState<string | undefined>()

const handleMount = useCallback<OnMount>(
(editor, monaco) => {
if (!jsonSchema) {
return
if (jsonSchema && format === 'json') {
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
enableSchemaRequest: true,
schemas: [
...(monaco.languages.json.jsonDefaults.diagnosticsOptions.schemas || []),
jsonSchema,
],
validate: true,
})

const uri = jsonSchema.uri
const newUri = uri.includes('?')
? `${uri}&${crypto.randomUUID ? crypto.randomUUID() : uuidv4()}`
: `${uri}?${crypto.randomUUID ? crypto.randomUUID() : uuidv4()}`

editor.setModel(
monaco.editor.createModel(
JSON.stringify(value, null, 2),
'json',
monaco.Uri.parse(newUri),
),
)
} else {
const language = format === 'yaml' ? 'yaml' : 'json'
editor.setModel(monaco.editor.createModel(initialStringValue || '', language))
}

monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
enableSchemaRequest: true,
schemas: [
...(monaco.languages.json.jsonDefaults.diagnosticsOptions.schemas || []),
jsonSchema,
],
validate: true,
})

const uri = jsonSchema.uri
const newUri = uri.includes('?')
? `${uri}&${crypto.randomUUID ? crypto.randomUUID() : uuidv4()}`
: `${uri}?${crypto.randomUUID ? crypto.randomUUID() : uuidv4()}`

editor.setModel(
monaco.editor.createModel(JSON.stringify(value, null, 2), 'json', monaco.Uri.parse(newUri)),
)
},
[jsonSchema, value],
[jsonSchema, value, format, initialStringValue],
)

const handleChange = useCallback(
(val) => {
async (val) => {
if (readOnly || disabled) {
return
}
inputChangeFromRef.current = 'user'

try {
setValue(val ? JSON.parse(val) : null)
const parsedValue = val ? await parseData(val, format) : null
setValue(parsedValue)
setJsonError(undefined)
} catch (e) {
setValue(val ? val : null)
setJsonError(e)
}
},
[readOnly, disabled, setValue],
[readOnly, disabled, setValue, format],
)

useEffect(() => {
if (inputChangeFromRef.current === 'system') {
setInitialStringValue(
(value || initialValue) !== undefined
? JSON.stringify(value ?? initialValue, null, 2)
: undefined,
)
setEditorKey(new Date().toString())
const updateStringValue = async () => {
if (inputChangeFromRef.current === 'system') {
if ((value || initialValue) !== undefined) {
const data = value ?? initialValue
const stringified = await stringifyData(data, format)
setInitialStringValue(stringified)
} else {
setInitialStringValue(undefined)
}
setEditorKey(new Date().toString())
}
}

void updateStringValue()
inputChangeFromRef.current = 'system'
}, [initialValue, value])
}, [initialValue, value, format])

const styles = useMemo(() => mergeFieldStyles(field), [field])

Expand Down Expand Up @@ -151,7 +159,7 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
/>
{BeforeInput}
<CodeEditor
defaultLanguage="json"
defaultLanguage={format === 'yaml' ? 'yaml' : 'json'}
key={editorKey}
maxHeight={maxHeight}
onChange={handleChange}
Expand Down
36 changes: 36 additions & 0 deletions packages/ui/src/fields/JSON/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use server'

import JSON5 from 'json5'
import yaml from 'yaml'

export function stringifyData(
data: unknown,
format: 'json' | 'json5' | 'yaml' = 'json',
): Promise<string> {
try {
if (format === 'yaml') {
return Promise.resolve(yaml.stringify(data))
} else if (format === 'json5') {
return Promise.resolve(JSON5.stringify(data, null, 2))
}
return Promise.resolve(JSON.stringify(data, null, 2))
} catch (error) {
return Promise.resolve(JSON.stringify(data, null, 2))
}
}

export function parseData(
text: string,
format: 'json' | 'json5' | 'yaml' = 'json',
): Promise<unknown> {
try {
if (format === 'yaml') {
return Promise.resolve(yaml.parse(text))
} else if (format === 'json5') {
return Promise.resolve(JSON5.parse(text))
}
return Promise.resolve(JSON.parse(text))
} catch (error) {
throw new Error(`Error parsing ${format}: ${error.message}`)
}
}
Loading