sanity-plugin-internationalized-array 3.1.4 → 3.1.6
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.
- package/lib/index.esm.js +161 -22
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +160 -21
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +161 -22
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/cache.ts +129 -1
- package/src/components/DocumentAddButtons.tsx +71 -3
- package/src/components/Feedback.tsx +2 -1
- package/src/components/InternationalizedArray.tsx +2 -2
- package/src/components/InternationalizedArrayContext.tsx +38 -5
- package/src/components/InternationalizedInput.tsx +93 -1
- package/src/components/Preload.tsx +16 -6
- package/src/schema/array.ts +53 -28
|
@@ -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(
|
|
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
|
-
//
|
|
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,12 +1,13 @@
|
|
|
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'
|
|
7
8
|
import {suspend} from 'suspend-react'
|
|
8
9
|
|
|
9
|
-
import {
|
|
10
|
+
import {createCacheKey, setFunctionCache} from '../cache'
|
|
10
11
|
import {CONFIG_DEFAULT} from '../constants'
|
|
11
12
|
import type {Language, PluginConfig} from '../types'
|
|
12
13
|
import DocumentAddButtons from './DocumentAddButtons'
|
|
@@ -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})
|
|
@@ -49,6 +50,27 @@ export function InternationalizedArrayProvider(
|
|
|
49
50
|
[internationalizedArray.select, deferredDocument]
|
|
50
51
|
)
|
|
51
52
|
|
|
53
|
+
// Use a stable workspace identifier to prevent unnecessary re-renders
|
|
54
|
+
const workspaceId = useMemo(() => {
|
|
55
|
+
// Use workspace name if available, otherwise create a stable hash
|
|
56
|
+
if (workspace?.name) {
|
|
57
|
+
return workspace.name
|
|
58
|
+
}
|
|
59
|
+
// Create a stable hash from workspace properties that matter for caching
|
|
60
|
+
const workspaceKey = {
|
|
61
|
+
name: workspace?.name,
|
|
62
|
+
title: workspace?.title,
|
|
63
|
+
// Add other stable properties as needed
|
|
64
|
+
}
|
|
65
|
+
return JSON.stringify(workspaceKey)
|
|
66
|
+
}, [workspace])
|
|
67
|
+
|
|
68
|
+
// Memoize the cache key to prevent expensive JSON.stringify calls
|
|
69
|
+
const cacheKey = useMemo(
|
|
70
|
+
() => createCacheKey(selectedValue, workspaceId),
|
|
71
|
+
[selectedValue, workspaceId]
|
|
72
|
+
)
|
|
73
|
+
|
|
52
74
|
// Fetch or return languages
|
|
53
75
|
const languages = Array.isArray(internationalizedArray.languages)
|
|
54
76
|
? internationalizedArray.languages
|
|
@@ -56,11 +78,22 @@ export function InternationalizedArrayProvider(
|
|
|
56
78
|
// eslint-disable-next-line require-await
|
|
57
79
|
async () => {
|
|
58
80
|
if (typeof internationalizedArray.languages === 'function') {
|
|
59
|
-
|
|
81
|
+
const result = await internationalizedArray.languages(
|
|
82
|
+
client,
|
|
83
|
+
selectedValue
|
|
84
|
+
)
|
|
85
|
+
// Populate function cache for use outside React context
|
|
86
|
+
setFunctionCache(
|
|
87
|
+
internationalizedArray.languages,
|
|
88
|
+
selectedValue,
|
|
89
|
+
result,
|
|
90
|
+
workspaceId
|
|
91
|
+
)
|
|
92
|
+
return result
|
|
60
93
|
}
|
|
61
94
|
return internationalizedArray.languages
|
|
62
95
|
},
|
|
63
|
-
|
|
96
|
+
cacheKey,
|
|
64
97
|
{equal}
|
|
65
98
|
)
|
|
66
99
|
|
|
@@ -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(
|
|
232
|
+
{props.inputProps.renderInput(inlineProps)}
|
|
141
233
|
</Card>
|
|
142
234
|
|
|
143
235
|
<Card tone="inherit">
|
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
import {memo} from 'react'
|
|
2
2
|
import {useClient} from 'sanity'
|
|
3
3
|
|
|
4
|
-
import {peek,
|
|
4
|
+
import {createCacheKey, peek, preloadWithKey, setFunctionCache} from '../cache'
|
|
5
5
|
import type {PluginConfig} from '../types'
|
|
6
6
|
|
|
7
7
|
export default memo(function Preload(
|
|
8
8
|
props: Required<Pick<PluginConfig, 'apiVersion' | 'languages'>>
|
|
9
9
|
) {
|
|
10
10
|
const client = useClient({apiVersion: props.apiVersion})
|
|
11
|
+
|
|
12
|
+
// Use the same cache key structure as the main component
|
|
13
|
+
// This should match the main component when selectedValue is empty
|
|
14
|
+
const cacheKey = createCacheKey({})
|
|
15
|
+
|
|
11
16
|
if (!Array.isArray(peek({}))) {
|
|
12
17
|
// eslint-disable-next-line require-await
|
|
13
|
-
|
|
14
|
-
Array.isArray(props.languages)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
preloadWithKey(async () => {
|
|
19
|
+
if (Array.isArray(props.languages)) {
|
|
20
|
+
return props.languages
|
|
21
|
+
}
|
|
22
|
+
const result = await props.languages(client, {})
|
|
23
|
+
// Populate function cache for sharing with other components
|
|
24
|
+
// Use the same key structure as the main component
|
|
25
|
+
setFunctionCache(props.languages, {}, result)
|
|
26
|
+
return result
|
|
27
|
+
}, cacheKey)
|
|
18
28
|
}
|
|
19
29
|
|
|
20
30
|
return null
|
package/src/schema/array.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable no-nested-ternary */
|
|
2
2
|
import {defineField, type FieldDefinition, type Rule} from 'sanity'
|
|
3
3
|
|
|
4
|
-
import {peek} from '../cache'
|
|
4
|
+
import {getFunctionCache, peek, setFunctionCache} from '../cache'
|
|
5
5
|
import {createFieldName} from '../components/createFieldName'
|
|
6
6
|
import {getSelectedValue} from '../components/getSelectedValue'
|
|
7
7
|
import InternationalizedArray from '../components/InternationalizedArray'
|
|
@@ -50,7 +50,12 @@ export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
|
|
|
50
50
|
// @ts-expect-error - fix typings
|
|
51
51
|
validation: (rule: Rule) =>
|
|
52
52
|
rule.custom<Value[]>(async (value, context) => {
|
|
53
|
-
if (!value) {
|
|
53
|
+
if (!value || value.length === 0) {
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Early return for simple cases to avoid expensive operations
|
|
58
|
+
if (value.length === 1 && !value[0]?._key) {
|
|
54
59
|
return true
|
|
55
60
|
}
|
|
56
61
|
|
|
@@ -65,7 +70,33 @@ export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
|
|
|
65
70
|
} else if (Array.isArray(peek(selectedValue))) {
|
|
66
71
|
contextLanguages = peek(selectedValue) || []
|
|
67
72
|
} else if (typeof languagesFieldOption === 'function') {
|
|
68
|
-
|
|
73
|
+
// Try to get from function cache first (if it's the same function as the component)
|
|
74
|
+
const cachedLanguages = getFunctionCache(
|
|
75
|
+
languagesFieldOption,
|
|
76
|
+
selectedValue
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if (Array.isArray(cachedLanguages)) {
|
|
80
|
+
contextLanguages = cachedLanguages
|
|
81
|
+
} else {
|
|
82
|
+
// Try suspend cache as fallback
|
|
83
|
+
const suspendCachedLanguages = peek(selectedValue)
|
|
84
|
+
if (Array.isArray(suspendCachedLanguages)) {
|
|
85
|
+
contextLanguages = suspendCachedLanguages
|
|
86
|
+
} else {
|
|
87
|
+
// Only make the async call if we don't have cached data
|
|
88
|
+
contextLanguages = await languagesFieldOption(
|
|
89
|
+
client,
|
|
90
|
+
selectedValue
|
|
91
|
+
)
|
|
92
|
+
// Cache the result for future validation calls
|
|
93
|
+
setFunctionCache(
|
|
94
|
+
languagesFieldOption,
|
|
95
|
+
selectedValue,
|
|
96
|
+
contextLanguages
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
69
100
|
}
|
|
70
101
|
|
|
71
102
|
if (value && value.length > contextLanguages.length) {
|
|
@@ -76,12 +107,13 @@ export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
|
|
|
76
107
|
}`
|
|
77
108
|
}
|
|
78
109
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
110
|
+
// Create a Set for faster language ID lookups
|
|
111
|
+
const languageIds = new Set(contextLanguages.map((lang) => lang.id))
|
|
112
|
+
|
|
113
|
+
// Check for invalid language keys
|
|
114
|
+
const nonLanguageKeys = value.filter(
|
|
115
|
+
(item) => item?._key && !languageIds.has(item._key)
|
|
116
|
+
)
|
|
85
117
|
if (nonLanguageKeys.length) {
|
|
86
118
|
return {
|
|
87
119
|
message: `Array item keys must be valid languages registered to the field type`,
|
|
@@ -89,27 +121,20 @@ export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
|
|
|
89
121
|
}
|
|
90
122
|
}
|
|
91
123
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
124
|
+
// Check for duplicate language keys (more efficient)
|
|
125
|
+
const seenKeys = new Set<string>()
|
|
126
|
+
const duplicateValues: Value[] = []
|
|
127
|
+
|
|
128
|
+
for (const item of value) {
|
|
129
|
+
if (item?._key) {
|
|
130
|
+
if (seenKeys.has(item._key)) {
|
|
131
|
+
duplicateValues.push(item)
|
|
132
|
+
} else {
|
|
133
|
+
seenKeys.add(item._key)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
95
136
|
}
|
|
96
137
|
|
|
97
|
-
const valuesByLanguage = value?.length
|
|
98
|
-
? value
|
|
99
|
-
.filter((item) => Boolean(item?._key))
|
|
100
|
-
.reduce((acc, cur) => {
|
|
101
|
-
if (acc[cur._key]) {
|
|
102
|
-
return {...acc, [cur._key]: [...acc[cur._key], cur]}
|
|
103
|
-
}
|
|
104
|
-
return {
|
|
105
|
-
...acc,
|
|
106
|
-
[cur._key]: [cur],
|
|
107
|
-
}
|
|
108
|
-
}, {} as KeyedValues)
|
|
109
|
-
: {}
|
|
110
|
-
const duplicateValues = Object.values(valuesByLanguage)
|
|
111
|
-
.filter((item) => item?.length > 1)
|
|
112
|
-
.flat()
|
|
113
138
|
if (duplicateValues.length) {
|
|
114
139
|
return {
|
|
115
140
|
message: 'There can only be one field per language',
|