Skip to content
This repository was archived by the owner on Feb 3, 2026. It is now read-only.

Commit c59dba0

Browse files
committed
fix: initialize array fields properly when adding locales
- Fix document-level add locale button for Portable Text fields - Detect array-type fields and initialize with [] instead of undefined - Add paste operation support for empty internationalized fields - Field-level and unstable_fieldAction buttons remain unchanged Resolves 'Attempt to apply insert patch to non-array value' error that occurred only with document-level locale addition buttons.
1 parent 90e807b commit c59dba0

File tree

2 files changed

+102
-3
lines changed

2 files changed

+102
-3
lines changed

src/components/DocumentAddButtons.tsx

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
isSanityDocument,
88
PatchEvent,
99
setIfMissing,
10+
useClient,
11+
useSchema,
1012
} from 'sanity'
1113
import {useDocumentPane} from 'sanity/structure'
1214

@@ -27,9 +29,53 @@ export default function DocumentAddButtons(props: DocumentAddButtonsProps) {
2729

2830
const toast = useToast()
2931
const {onChange} = useDocumentPane()
32+
const schema = useSchema()
3033

3134
const documentsToTranslation = getDocumentsToTranslate(value, [])
3235

36+
// Helper function to determine if a field should be initialized as an array
37+
const getInitialValueForType = useCallback((typeName: string): any => {
38+
if (!typeName) return undefined
39+
40+
// Extract the base type name from internationalized array type
41+
// e.g., "internationalizedArrayBodyValue" -> "body"
42+
const match = typeName.match(/^internationalizedArray(.+)Value$/)
43+
if (!match) return undefined
44+
45+
const baseTypeName = match[1].charAt(0).toLowerCase() + match[1].slice(1)
46+
47+
// Check if it's a known array-based type (Portable Text fields)
48+
const arrayBasedTypes = ['body', 'htmlContent', 'blockContent', 'portableText']
49+
if (arrayBasedTypes.includes(baseTypeName)) {
50+
return []
51+
}
52+
53+
// Try to look up the schema type to determine if it's an array
54+
try {
55+
const schemaType = schema.get(typeName)
56+
if (schemaType) {
57+
// Check if this is an object type with a 'value' field
58+
const valueField = (schemaType as any)?.fields?.find((f: any) => f.name === 'value')
59+
if (valueField) {
60+
const fieldType = valueField.type
61+
// Check if the value field is an array type
62+
if (fieldType?.jsonType === 'array' ||
63+
fieldType?.name === 'array' ||
64+
fieldType?.type === 'array' ||
65+
fieldType?.of !== undefined ||
66+
arrayBasedTypes.includes(fieldType?.name)) {
67+
return []
68+
}
69+
}
70+
}
71+
} catch (error) {
72+
// If we can't determine from schema, fall back to undefined
73+
console.warn('Could not determine field type from schema:', typeName, error)
74+
}
75+
76+
return undefined
77+
}, [schema])
78+
3379
const handleDocumentButtonClick = useCallback(
3480
async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
3581
const languageId = event.currentTarget.value
@@ -77,13 +123,16 @@ export default function DocumentAddButtons(props: DocumentAddButtonsProps) {
77123
for (const toTranslate of removeDuplicates) {
78124
const path = toTranslate.path
79125

126+
// Get the appropriate initial value for this field type
127+
const initialValue = getInitialValueForType(toTranslate._type)
128+
80129
const ifMissing = setIfMissing([], path)
81130
const insertValue = insert(
82131
[
83132
{
84133
_key: languageId,
85134
_type: toTranslate._type,
86-
value: undefined,
135+
value: initialValue, // Use the determined initial value instead of undefined
87136
},
88137
],
89138
'after',
@@ -95,7 +144,7 @@ export default function DocumentAddButtons(props: DocumentAddButtonsProps) {
95144

96145
onChange(PatchEvent.from(patches.flat()))
97146
},
98-
[documentsToTranslation, onChange, toast]
147+
[documentsToTranslation, getInitialValueForType, onChange, toast]
99148
)
100149
return (
101150
<Stack space={3}>

src/components/InternationalizedInput.tsx

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,54 @@ export default function InternationalizedInput(
3434
props.path.slice(0, -1)
3535
) as InternationalizedValue[]
3636

37+
// Extract the original onChange to avoid dependency issues
38+
const originalOnChange = props.inputProps.onChange
39+
40+
// Create a wrapped onChange handler to intercept patches for paste operations
41+
const wrappedOnChange = useCallback((patches: any) => {
42+
// Check if this is a paste operation into an empty or uninitialized Portable Text field
43+
const valueField = props.value?.value
44+
const isEmptyOrUndefined = valueField === undefined ||
45+
(Array.isArray(valueField) && valueField.length === 0)
46+
47+
if (isEmptyOrUndefined) {
48+
// Check for insert patches that are trying to operate on a non-existent structure
49+
const hasProblematicInsert = patches.some((patch: any) => {
50+
// Look for insert patches targeting the value field or direct array index
51+
if (patch.type === 'insert' && patch.path && patch.path.length > 0) {
52+
// The path might be ['value', index] or just [index] depending on context
53+
const isTargetingValue = patch.path[0] === 'value' || typeof patch.path[0] === 'number'
54+
return isTargetingValue
55+
}
56+
return false
57+
})
58+
59+
if (hasProblematicInsert) {
60+
// First, ensure the value field exists as an empty array if it doesn't
61+
const initPatch = valueField === undefined ? { type: 'setIfMissing', path: ['value'], value: [] } : null
62+
63+
// Transform the patches to ensure they work with the nested structure
64+
const fixedPatches = patches.map((patch: any) => {
65+
if (patch.type === 'insert' && patch.path) {
66+
// Ensure the path is correct for the nested structure
67+
const fixedPath = patch.path[0] === 'value' ? patch.path : ['value', ...patch.path]
68+
const fixedPatch = { ...patch, path: fixedPath }
69+
return fixedPatch
70+
}
71+
return patch
72+
})
73+
74+
// If we need to initialize the field, include that patch first
75+
const allPatches = initPatch ? [initPatch, ...fixedPatches] : fixedPatches
76+
77+
return originalOnChange(allPatches)
78+
}
79+
}
80+
81+
// For all other cases, pass through unchanged
82+
return originalOnChange(patches)
83+
}, [props.value, originalOnChange])
84+
3785
const inlineProps = {
3886
...props.inputProps,
3987
// This is the magic that makes inline editing work?
@@ -43,6 +91,8 @@ export default function InternationalizedInput(
4391
// This just overrides the type
4492
// Remove this as it shouldn't be necessary?
4593
value: props.value as InternationalizedValue,
94+
// Use our wrapped onChange handler
95+
onChange: wrappedOnChange,
4696
}
4797

4898
const {validation, value, onChange, readOnly} = inlineProps
@@ -137,7 +187,7 @@ export default function InternationalizedInput(
137187
</Card>
138188
<Flex align="center" gap={2}>
139189
<Card flex={1} tone="inherit">
140-
{props.inputProps.renderInput(props.inputProps)}
190+
{props.inputProps.renderInput(inlineProps)}
141191
</Card>
142192

143193
<Card tone="inherit">

0 commit comments

Comments
 (0)