sanity-plugin-internationalized-array 3.1.5 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@ import {
7
7
  isSanityDocument,
8
8
  PatchEvent,
9
9
  setIfMissing,
10
+ useSchema,
10
11
  } from 'sanity'
11
12
  import {useDocumentPane} from 'sanity/structure'
12
13
 
@@ -21,15 +22,79 @@ type DocumentAddButtonsProps = {
21
22
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
23
  value: Record<string, any> | undefined
23
24
  }
24
- export default function DocumentAddButtons(props: DocumentAddButtonsProps) {
25
+ export default function DocumentAddButtons(
26
+ props: DocumentAddButtonsProps
27
+ ): React.ReactElement {
25
28
  const {filteredLanguages} = useInternationalizedArrayContext()
26
29
  const value = isSanityDocument(props.value) ? props.value : undefined
27
30
 
28
31
  const toast = useToast()
29
32
  const {onChange} = useDocumentPane()
33
+ const schema = useSchema()
30
34
 
31
35
  const documentsToTranslation = getDocumentsToTranslate(value, [])
32
36
 
37
+ // Helper function to determine if a field should be initialized as an array
38
+ const getInitialValueForType = useCallback(
39
+ (typeName: string): unknown => {
40
+ if (!typeName) return undefined
41
+
42
+ // Extract the base type name from internationalized array type
43
+ // e.g., "internationalizedArrayBodyValue" -> "body"
44
+ const match = typeName.match(/^internationalizedArray(.+)Value$/)
45
+ if (!match) return undefined
46
+
47
+ const baseTypeName = match[1].charAt(0).toLowerCase() + match[1].slice(1)
48
+
49
+ // Check if it's a known array-based type (Portable Text fields)
50
+ const arrayBasedTypes = [
51
+ 'body',
52
+ 'htmlContent',
53
+ 'blockContent',
54
+ 'portableText',
55
+ ]
56
+ if (arrayBasedTypes.includes(baseTypeName)) {
57
+ return []
58
+ }
59
+
60
+ // Try to look up the schema type to determine if it's an array
61
+ try {
62
+ const schemaType = schema.get(typeName)
63
+ if (schemaType) {
64
+ // Check if this is an object type with a 'value' field
65
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
+ const valueField = (schemaType as any)?.fields?.find(
67
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
+ (f: any) => f.name === 'value'
69
+ )
70
+ if (valueField) {
71
+ const fieldType = valueField.type
72
+ // Check if the value field is an array type
73
+ if (
74
+ fieldType?.jsonType === 'array' ||
75
+ fieldType?.name === 'array' ||
76
+ fieldType?.type === 'array' ||
77
+ fieldType?.of !== undefined ||
78
+ arrayBasedTypes.includes(fieldType?.name)
79
+ ) {
80
+ return []
81
+ }
82
+ }
83
+ }
84
+ } catch (error) {
85
+ // If we can't determine from schema, fall back to undefined
86
+ console.warn(
87
+ 'Could not determine field type from schema:',
88
+ typeName,
89
+ error
90
+ )
91
+ }
92
+
93
+ return undefined
94
+ },
95
+ [schema]
96
+ )
97
+
33
98
  const handleDocumentButtonClick = useCallback(
34
99
  async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
35
100
  const languageId = event.currentTarget.value
@@ -77,13 +142,16 @@ export default function DocumentAddButtons(props: DocumentAddButtonsProps) {
77
142
  for (const toTranslate of removeDuplicates) {
78
143
  const path = toTranslate.path
79
144
 
145
+ // Get the appropriate initial value for this field type
146
+ const initialValue = getInitialValueForType(toTranslate._type)
147
+
80
148
  const ifMissing = setIfMissing([], path)
81
149
  const insertValue = insert(
82
150
  [
83
151
  {
84
152
  _key: languageId,
85
153
  _type: toTranslate._type,
86
- value: undefined,
154
+ value: initialValue, // Use the determined initial value instead of undefined
87
155
  },
88
156
  ],
89
157
  'after',
@@ -95,7 +163,7 @@ export default function DocumentAddButtons(props: DocumentAddButtonsProps) {
95
163
 
96
164
  onChange(PatchEvent.from(patches.flat()))
97
165
  },
98
- [documentsToTranslation, onChange, toast]
166
+ [documentsToTranslation, getInitialValueForType, onChange, toast]
99
167
  )
100
168
  return (
101
169
  <Stack space={3}>
@@ -1,4 +1,5 @@
1
1
  import {Card, Code, Stack, Text} from '@sanity/ui'
2
+ import type React from 'react'
2
3
 
3
4
  const schemaExample = {
4
5
  languages: [
@@ -7,7 +8,7 @@ const schemaExample = {
7
8
  ],
8
9
  }
9
10
 
10
- export default function Feedback() {
11
+ export default function Feedback(): React.ReactElement {
11
12
  return (
12
13
  <Card tone="caution" border radius={2} padding={3}>
13
14
  <Stack space={4}>
@@ -29,7 +29,7 @@ export type InternationalizedArrayProps = ArrayOfObjectsInputProps<
29
29
 
30
30
  export default function InternationalizedArray(
31
31
  props: InternationalizedArrayProps
32
- ) {
32
+ ): React.ReactElement {
33
33
  const {members, value, schemaType, onChange} = props
34
34
 
35
35
  const readOnly =
@@ -131,7 +131,7 @@ export default function InternationalizedArray(
131
131
  languages,
132
132
  ])
133
133
 
134
- // TODO: This is reordering and re-setting the whole array, it could be surgical
134
+ // NOTE: This is reordering and re-setting the whole array, it could be surgical
135
135
  const handleRestoreOrder = useCallback(() => {
136
136
  if (!value?.length || !languages?.length) {
137
137
  return
@@ -1,6 +1,7 @@
1
1
  import {useLanguageFilterStudioContext} from '@sanity/language-filter'
2
2
  import {Stack} from '@sanity/ui'
3
3
  import equal from 'fast-deep-equal'
4
+ import type React from 'react'
4
5
  import {createContext, useContext, useDeferredValue, useMemo} from 'react'
5
6
  import {type ObjectInputProps, useClient, useWorkspace} from 'sanity'
6
7
  import {useDocumentPane} from 'sanity/structure'
@@ -27,7 +28,7 @@ export const InternationalizedArrayContext =
27
28
  filteredLanguages: [],
28
29
  })
29
30
 
30
- export function useInternationalizedArrayContext() {
31
+ export function useInternationalizedArrayContext(): InternationalizedArrayContextProps {
31
32
  return useContext(InternationalizedArrayContext)
32
33
  }
33
34
 
@@ -37,7 +38,7 @@ type InternationalizedArrayProviderProps = ObjectInputProps & {
37
38
 
38
39
  export function InternationalizedArrayProvider(
39
40
  props: InternationalizedArrayProviderProps
40
- ) {
41
+ ): React.ReactElement {
41
42
  const {internationalizedArray} = props
42
43
 
43
44
  const client = useClient({apiVersion: internationalizedArray.apiVersion})
@@ -34,6 +34,96 @@ export default function InternationalizedInput(
34
34
  props.path.slice(0, -1)
35
35
  ) as InternationalizedValue[]
36
36
 
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(
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
+ (patches: any) => {
44
+ // Ensure patches is an array before proceeding with paste logic
45
+ // For single patch operations (like unset), pass through directly
46
+ if (!Array.isArray(patches)) {
47
+ return originalOnChange(patches)
48
+ }
49
+
50
+ // Check if this is a paste operation into an empty or uninitialized Portable Text field
51
+ const valueField = props.value?.value
52
+ const isEmptyOrUndefined =
53
+ valueField === undefined ||
54
+ valueField === null ||
55
+ (Array.isArray(valueField) && valueField.length === 0)
56
+
57
+ if (isEmptyOrUndefined) {
58
+ // Check for insert patches that are trying to operate on a non-existent structure
59
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
+ const hasProblematicInsert = patches.some((patch: any) => {
61
+ // Ensure patch exists and has required properties
62
+ if (!patch || typeof patch !== 'object') {
63
+ return false
64
+ }
65
+
66
+ // Look for insert patches targeting the value field or direct array index
67
+ if (
68
+ patch.type === 'insert' &&
69
+ patch.path &&
70
+ Array.isArray(patch.path) &&
71
+ patch.path.length > 0
72
+ ) {
73
+ // The path might be ['value', index] or just [index] depending on context
74
+ const isTargetingValue =
75
+ patch.path[0] === 'value' || typeof patch.path[0] === 'number'
76
+ return isTargetingValue
77
+ }
78
+ return false
79
+ })
80
+
81
+ if (hasProblematicInsert) {
82
+ // First, ensure the value field exists as an empty array if it doesn't
83
+ const initPatch =
84
+ valueField === undefined
85
+ ? {type: 'setIfMissing', path: ['value'], value: []}
86
+ : null
87
+
88
+ // Transform the patches to ensure they work with the nested structure
89
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
90
+ const fixedPatches = patches.map((patch: any) => {
91
+ // Ensure patch exists and has required properties
92
+ if (!patch || typeof patch !== 'object') {
93
+ return patch
94
+ }
95
+
96
+ if (
97
+ patch.type === 'insert' &&
98
+ patch.path &&
99
+ Array.isArray(patch.path)
100
+ ) {
101
+ // Ensure the path is correct for the nested structure
102
+ const fixedPath =
103
+ patch.path[0] === 'value'
104
+ ? patch.path
105
+ : ['value', ...patch.path]
106
+ const fixedPatch = {...patch, path: fixedPath}
107
+ return fixedPatch
108
+ }
109
+ return patch
110
+ })
111
+
112
+ // If we need to initialize the field, include that patch first
113
+ const allPatches = initPatch
114
+ ? [initPatch, ...fixedPatches]
115
+ : fixedPatches
116
+
117
+ return originalOnChange(allPatches)
118
+ }
119
+ }
120
+
121
+ // For all other cases, pass through unchanged
122
+ return originalOnChange(patches)
123
+ },
124
+ [props.value, originalOnChange]
125
+ )
126
+
37
127
  const inlineProps = {
38
128
  ...props.inputProps,
39
129
  // This is the magic that makes inline editing work?
@@ -43,6 +133,8 @@ export default function InternationalizedInput(
43
133
  // This just overrides the type
44
134
  // Remove this as it shouldn't be necessary?
45
135
  value: props.value as InternationalizedValue,
136
+ // Use our wrapped onChange handler
137
+ onChange: wrappedOnChange,
46
138
  }
47
139
 
48
140
  const {validation, value, onChange, readOnly} = inlineProps
@@ -137,7 +229,7 @@ export default function InternationalizedInput(
137
229
  </Card>
138
230
  <Flex align="center" gap={2}>
139
231
  <Card flex={1} tone="inherit">
140
- {props.inputProps.renderInput(props.inputProps)}
232
+ {props.inputProps.renderInput(inlineProps)}
141
233
  </Card>
142
234
 
143
235
  <Card tone="inherit">
package/src/constants.ts CHANGED
@@ -11,7 +11,7 @@ export const CONFIG_DEFAULT: Required<PluginConfig> = {
11
11
  select: {},
12
12
  defaultLanguages: [],
13
13
  fieldTypes: [],
14
- apiVersion: '2022-11-27',
14
+ apiVersion: '2025-10-15',
15
15
  buttonLocations: ['field'],
16
16
  buttonAddAll: true,
17
17
  languageDisplay: 'codeOnly',
package/src/plugin.tsx CHANGED
@@ -13,7 +13,7 @@ import {flattenSchemaType} from './utils/flattenSchemaType'
13
13
  export const internationalizedArray = definePlugin<PluginConfig>((config) => {
14
14
  const pluginConfig = {...CONFIG_DEFAULT, ...config}
15
15
  const {
16
- apiVersion = '2022-11-27',
16
+ apiVersion = '2025-10-15',
17
17
  select,
18
18
  languages,
19
19
  fieldTypes,
package/src/types.ts CHANGED
@@ -40,7 +40,7 @@ export type LanguageDisplay = 'titleOnly' | 'codeOnly' | 'titleAndCode'
40
40
  export type PluginConfig = {
41
41
  /**
42
42
  * https://www.sanity.io/docs/api-versioning
43
- * @defaultValue '2022-11-27'
43
+ * @defaultValue '2025-10-15'
44
44
  */
45
45
  apiVersion?: string
46
46
  /**