sanity-plugin-internationalized-array 3.2.2 → 4.0.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.
Files changed (43) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +3 -34
  3. package/{lib → dist}/index.d.ts +41 -61
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +882 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +36 -74
  8. package/lib/index.d.mts +0 -149
  9. package/lib/index.esm.js +0 -854
  10. package/lib/index.esm.js.map +0 -1
  11. package/lib/index.js +0 -863
  12. package/lib/index.js.map +0 -1
  13. package/lib/index.mjs +0 -854
  14. package/lib/index.mjs.map +0 -1
  15. package/sanity.json +0 -8
  16. package/src/cache.ts +0 -148
  17. package/src/components/AddButtons.tsx +0 -60
  18. package/src/components/DocumentAddButtons.tsx +0 -183
  19. package/src/components/Feedback.tsx +0 -28
  20. package/src/components/InternationalizedArray.tsx +0 -286
  21. package/src/components/InternationalizedArrayContext.tsx +0 -136
  22. package/src/components/InternationalizedField.tsx +0 -57
  23. package/src/components/InternationalizedInput.tsx +0 -257
  24. package/src/components/Preload.tsx +0 -31
  25. package/src/components/createFieldName.ts +0 -20
  26. package/src/components/getSelectedValue.ts +0 -31
  27. package/src/components/getToneFromValidation.ts +0 -20
  28. package/src/constants.ts +0 -18
  29. package/src/fieldActions/index.ts +0 -138
  30. package/src/index.ts +0 -3
  31. package/src/plugin.tsx +0 -87
  32. package/src/schema/array.ts +0 -148
  33. package/src/schema/object.ts +0 -36
  34. package/src/types.ts +0 -135
  35. package/src/utils/checkAllLanguagesArePresent.ts +0 -14
  36. package/src/utils/createAddAllTitle.ts +0 -16
  37. package/src/utils/createAddLanguagePatches.ts +0 -84
  38. package/src/utils/createValueSchemaTypeName.ts +0 -5
  39. package/src/utils/flattenSchemaType.ts +0 -63
  40. package/src/utils/getDocumentsToTranslate.ts +0 -66
  41. package/src/utils/getLanguageDisplay.ts +0 -13
  42. package/src/utils/getLanguagesFieldOption.ts +0 -16
  43. package/v2-incompatible.js +0 -11
@@ -1,257 +0,0 @@
1
- import {RemoveCircleIcon} from '@sanity/icons'
2
- import {
3
- Button,
4
- Card,
5
- Flex,
6
- Label,
7
- Menu,
8
- MenuButton,
9
- MenuItem,
10
- Spinner,
11
- Stack,
12
- Text,
13
- Tooltip,
14
- } from '@sanity/ui'
15
- import type React from 'react'
16
- import {ReactNode, useCallback, useMemo} from 'react'
17
- import {type ObjectItemProps, useFormValue} from 'sanity'
18
- import {set, unset} from 'sanity'
19
-
20
- import {getLanguageDisplay} from '../utils/getLanguageDisplay'
21
- import {getToneFromValidation} from './getToneFromValidation'
22
- import {useInternationalizedArrayContext} from './InternationalizedArrayContext'
23
-
24
- export type InternationalizedValue = {
25
- _type: string
26
- _key: string
27
- value: string
28
- }
29
-
30
- export default function InternationalizedInput(
31
- props: ObjectItemProps<InternationalizedValue>
32
- ): ReactNode {
33
- const parentValue = useFormValue(
34
- props.path.slice(0, -1)
35
- ) as InternationalizedValue[]
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
-
127
- const inlineProps = {
128
- ...props.inputProps,
129
- // This is the magic that makes inline editing work?
130
- members: props.inputProps.members.filter(
131
- (m) => m.kind === 'field' && m.name === 'value'
132
- ),
133
- // This just overrides the type
134
- // Remove this as it shouldn't be necessary?
135
- value: props.value as InternationalizedValue,
136
- // Use our wrapped onChange handler
137
- onChange: wrappedOnChange,
138
- }
139
-
140
- const {validation, value, onChange, readOnly} = inlineProps
141
-
142
- // The parent array contains the languages from the plugin config
143
- const {languages, languageDisplay, defaultLanguages} =
144
- useInternationalizedArrayContext()
145
-
146
- const languageKeysInUse = useMemo(
147
- () => parentValue?.map((v) => v._key) ?? [],
148
- [parentValue]
149
- )
150
- const keyIsValid = languages?.length
151
- ? languages.find((l) => l.id === value._key)
152
- : false
153
-
154
- // Changes the key of this item, ideally to a valid language
155
- const handleKeyChange = useCallback(
156
- (event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
157
- const languageId = event?.currentTarget?.value
158
-
159
- if (
160
- !value ||
161
- !languages?.length ||
162
- !languages.find((l) => l.id === languageId)
163
- ) {
164
- return
165
- }
166
-
167
- onChange([set(languageId, ['_key'])])
168
- },
169
- [onChange, value, languages]
170
- )
171
-
172
- // Removes this item from the array
173
- const handleUnset = useCallback((): void => {
174
- onChange(unset())
175
- }, [onChange])
176
-
177
- if (!languages) {
178
- return <Spinner />
179
- }
180
-
181
- const language = languages.find((l) => l.id === value._key)
182
- const languageTitle: string =
183
- keyIsValid && language
184
- ? getLanguageDisplay(languageDisplay, language.title, language.id)
185
- : ''
186
-
187
- const isDefault = defaultLanguages.includes(value._key)
188
-
189
- const removeButton = (
190
- <Button
191
- mode="bleed"
192
- icon={RemoveCircleIcon}
193
- tone="critical"
194
- disabled={readOnly || isDefault}
195
- onClick={handleUnset}
196
- />
197
- )
198
-
199
- return (
200
- <Card paddingTop={2} tone={getToneFromValidation(validation)}>
201
- <Stack space={2}>
202
- <Card tone="inherit">
203
- {keyIsValid ? (
204
- <Label muted size={1}>
205
- {languageTitle}
206
- </Label>
207
- ) : (
208
- <MenuButton
209
- button={<Button fontSize={1} text={`Change "${value._key}"`} />}
210
- id={`${value._key}-change-key`}
211
- menu={
212
- <Menu>
213
- {languages.map((lang) => (
214
- <MenuItem
215
- disabled={languageKeysInUse.includes(lang.id)}
216
- fontSize={1}
217
- key={lang.id}
218
- text={lang.id.toLocaleUpperCase()}
219
- value={lang.id}
220
- // @ts-expect-error - fix typings
221
- onClick={handleKeyChange}
222
- />
223
- ))}
224
- </Menu>
225
- }
226
- popover={{portal: true}}
227
- />
228
- )}
229
- </Card>
230
- <Flex align="center" gap={2}>
231
- <Card flex={1} tone="inherit">
232
- {props.inputProps.renderInput(inlineProps)}
233
- </Card>
234
-
235
- <Card tone="inherit">
236
- {isDefault ? (
237
- <Tooltip
238
- content={
239
- <Text muted size={1}>
240
- Can&apos;t remove default language
241
- </Text>
242
- }
243
- fallbackPlacements={['right', 'left']}
244
- placement="top"
245
- portal
246
- >
247
- <span>{removeButton}</span>
248
- </Tooltip>
249
- ) : (
250
- removeButton
251
- )}
252
- </Card>
253
- </Flex>
254
- </Stack>
255
- </Card>
256
- )
257
- }
@@ -1,31 +0,0 @@
1
- import {memo} from 'react'
2
- import {useClient} from 'sanity'
3
-
4
- import {createCacheKey, peek, preloadWithKey, setFunctionCache} from '../cache'
5
- import type {PluginConfig} from '../types'
6
-
7
- export default memo(function Preload(
8
- props: Required<Pick<PluginConfig, 'apiVersion' | 'languages'>>
9
- ) {
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
-
16
- if (!Array.isArray(peek({}))) {
17
- // eslint-disable-next-line require-await
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)
28
- }
29
-
30
- return null
31
- })
@@ -1,20 +0,0 @@
1
- export function camelCase(string: string): string {
2
- return string.replace(/-([a-z])/g, (g) => g[1].toUpperCase())
3
- }
4
-
5
- export function titleCase(string: string): string {
6
- return string
7
- .split(` `)
8
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
9
- .join(` `)
10
- }
11
-
12
- export function pascalCase(string: string): string {
13
- return titleCase(camelCase(string))
14
- }
15
-
16
- export function createFieldName(name: string, addValue = false): string {
17
- return addValue
18
- ? [`internationalizedArray`, pascalCase(name), `Value`].join(``)
19
- : [`internationalizedArray`, pascalCase(name)].join(``)
20
- }
@@ -1,31 +0,0 @@
1
- import {get} from 'lodash'
2
-
3
- export const getSelectedValue = (
4
- select: Record<string, string> | undefined,
5
- document:
6
- | {
7
- [x: string]: unknown
8
- }
9
- | undefined
10
- ): Record<string, unknown> => {
11
- if (!select || !document) {
12
- return {}
13
- }
14
-
15
- const selection: Record<string, string> = select || {}
16
- const selectedValue: Record<string, unknown> = {}
17
- for (const [key, path] of Object.entries(selection)) {
18
- let value = get(document, path)
19
- if (Array.isArray(value)) {
20
- // If there are references in the array, ensure they have `_ref` set, otherwise they are considered empty and can safely be ignored
21
- value = value.filter((item) =>
22
- typeof item === 'object'
23
- ? item?._type === 'reference' && '_ref' in item
24
- : true
25
- )
26
- }
27
- selectedValue[key] = value
28
- }
29
-
30
- return selectedValue
31
- }
@@ -1,20 +0,0 @@
1
- import type {CardTone} from '@sanity/ui'
2
- import type {FormNodeValidation} from 'sanity'
3
-
4
- export function getToneFromValidation(
5
- validations: FormNodeValidation[]
6
- ): CardTone | undefined {
7
- if (!validations?.length) {
8
- return undefined
9
- }
10
-
11
- const validationLevels = validations.map((v) => v.level)
12
-
13
- if (validationLevels.includes('error')) {
14
- return `critical`
15
- } else if (validationLevels.includes('warning')) {
16
- return `caution`
17
- }
18
-
19
- return undefined
20
- }
package/src/constants.ts DELETED
@@ -1,18 +0,0 @@
1
- import {PluginConfig} from './types'
2
-
3
- export const MAX_COLUMNS = {
4
- codeOnly: 5,
5
- titleOnly: 4,
6
- titleAndCode: 3,
7
- }
8
-
9
- export const CONFIG_DEFAULT: Required<PluginConfig> = {
10
- languages: [],
11
- select: {},
12
- defaultLanguages: [],
13
- fieldTypes: [],
14
- apiVersion: '2025-10-15',
15
- buttonLocations: ['field'],
16
- buttonAddAll: true,
17
- languageDisplay: 'codeOnly',
18
- }
@@ -1,138 +0,0 @@
1
- import {AddIcon, TranslateIcon} from '@sanity/icons'
2
- import {useCallback} from 'react'
3
- import {
4
- defineDocumentFieldAction,
5
- type DocumentFieldActionItem,
6
- type DocumentFieldActionProps,
7
- PatchEvent,
8
- setIfMissing,
9
- useFormValue,
10
- } from 'sanity'
11
- import {useDocumentPane} from 'sanity/structure'
12
-
13
- import {useInternationalizedArrayContext} from '../components/InternationalizedArrayContext'
14
- import type {Language, Value} from '../types'
15
- import {checkAllLanguagesArePresent} from '../utils/checkAllLanguagesArePresent'
16
- import {createAddAllTitle} from '../utils/createAddAllTitle'
17
- import {createAddLanguagePatches} from '../utils/createAddLanguagePatches'
18
-
19
- const createTranslateFieldActions: (
20
- fieldActionProps: DocumentFieldActionProps,
21
- context: {
22
- languages: Language[]
23
- filteredLanguages: Language[]
24
- }
25
- ) => DocumentFieldActionItem[] = (
26
- fieldActionProps,
27
- {languages, filteredLanguages}
28
- ) =>
29
- languages.map((language) => {
30
- const value = useFormValue(fieldActionProps.path) as Value[]
31
- const disabled =
32
- value && Array.isArray(value)
33
- ? Boolean(value?.find((item) => item._key === language.id))
34
- : false
35
- const hidden = !filteredLanguages.some((f) => f.id === language.id)
36
-
37
- const {onChange} = useDocumentPane()
38
-
39
- const onAction = useCallback(() => {
40
- const {schemaType, path} = fieldActionProps
41
-
42
- const addLanguageKeys = [language.id]
43
- const patches = createAddLanguagePatches({
44
- addLanguageKeys,
45
- schemaType,
46
- languages,
47
- filteredLanguages,
48
- value,
49
- path,
50
- })
51
-
52
- onChange(PatchEvent.from([setIfMissing([], path), ...patches]))
53
- }, [language.id, value, onChange])
54
-
55
- return {
56
- type: 'action',
57
- icon: AddIcon,
58
- onAction,
59
- title: language.title,
60
- hidden,
61
- disabled,
62
- }
63
- })
64
-
65
- const AddMissingTranslationsFieldAction: (
66
- fieldActionProps: DocumentFieldActionProps,
67
- context: {
68
- languages: Language[]
69
- filteredLanguages: Language[]
70
- }
71
- ) => DocumentFieldActionItem = (
72
- fieldActionProps,
73
- {languages, filteredLanguages}
74
- ) => {
75
- const value = useFormValue(fieldActionProps.path) as Value[]
76
- const disabled = value && value.length === filteredLanguages.length
77
- const hidden = checkAllLanguagesArePresent(filteredLanguages, value)
78
-
79
- const {onChange} = useDocumentPane()
80
-
81
- const onAction = useCallback(() => {
82
- const {schemaType, path} = fieldActionProps
83
-
84
- const addLanguageKeys: string[] = []
85
- const patches = createAddLanguagePatches({
86
- addLanguageKeys,
87
- schemaType,
88
- languages,
89
- filteredLanguages,
90
- value,
91
- path,
92
- })
93
-
94
- onChange(PatchEvent.from([setIfMissing([], path), ...patches]))
95
- }, [fieldActionProps, filteredLanguages, languages, onChange, value])
96
-
97
- return {
98
- type: 'action',
99
- icon: AddIcon,
100
- onAction,
101
- title: createAddAllTitle(value, filteredLanguages),
102
- disabled,
103
- hidden,
104
- }
105
- }
106
-
107
- export const internationalizedArrayFieldAction = defineDocumentFieldAction({
108
- name: 'internationalizedArray',
109
- useAction(fieldActionProps) {
110
- const isInternationalizedArrayField =
111
- fieldActionProps?.schemaType?.type?.name.startsWith(
112
- 'internationalizedArray'
113
- )
114
- const {languages, filteredLanguages} = useInternationalizedArrayContext()
115
-
116
- const translateFieldActions = createTranslateFieldActions(
117
- fieldActionProps,
118
- {languages, filteredLanguages}
119
- )
120
-
121
- return {
122
- type: 'group',
123
- icon: TranslateIcon,
124
- title: 'Add Translation',
125
- renderAsButton: true,
126
- children: isInternationalizedArrayField
127
- ? [
128
- ...translateFieldActions,
129
- AddMissingTranslationsFieldAction(fieldActionProps, {
130
- languages,
131
- filteredLanguages,
132
- }),
133
- ]
134
- : [],
135
- hidden: !isInternationalizedArrayField,
136
- }
137
- },
138
- })
package/src/index.ts DELETED
@@ -1,3 +0,0 @@
1
- export {clear} from './cache'
2
- export {internationalizedArray} from './plugin'
3
- export * from './types'
package/src/plugin.tsx DELETED
@@ -1,87 +0,0 @@
1
- import {definePlugin, isObjectInputProps} from 'sanity'
2
-
3
- import {InternationalizedArrayProvider} from './components/InternationalizedArrayContext'
4
- import InternationalizedField from './components/InternationalizedField'
5
- import Preload from './components/Preload'
6
- import {CONFIG_DEFAULT} from './constants'
7
- import {internationalizedArrayFieldAction} from './fieldActions'
8
- import array from './schema/array'
9
- import object from './schema/object'
10
- import {PluginConfig} from './types'
11
- import {flattenSchemaType} from './utils/flattenSchemaType'
12
-
13
- export const internationalizedArray = definePlugin<PluginConfig>((config) => {
14
- const pluginConfig = {...CONFIG_DEFAULT, ...config}
15
- const {
16
- apiVersion = '2025-10-15',
17
- select,
18
- languages,
19
- fieldTypes,
20
- defaultLanguages,
21
- buttonLocations,
22
- } = pluginConfig
23
-
24
- return {
25
- name: 'sanity-plugin-internationalized-array',
26
- // Preload languages for use throughout the Studio
27
- studio: Array.isArray(languages)
28
- ? undefined
29
- : {
30
- components: {
31
- layout: (props) => (
32
- <>
33
- <Preload apiVersion={apiVersion} languages={languages} />
34
- {props.renderDefault(props)}
35
- </>
36
- ),
37
- },
38
- },
39
- // Optional: render "add language" buttons as field actions
40
- document: {
41
- unstable_fieldActions: buttonLocations.includes('unstable__fieldAction')
42
- ? (prev) => [...prev, internationalizedArrayFieldAction]
43
- : undefined,
44
- },
45
- // Wrap document editor with a language provider
46
- form: {
47
- components: {
48
- field: (props) => <InternationalizedField {...props} />,
49
-
50
- input: (props) => {
51
- const isRootInput = props.id === 'root' && isObjectInputProps(props)
52
-
53
- if (!isRootInput) {
54
- return props.renderDefault(props)
55
- }
56
-
57
- const flatFieldTypeNames = flattenSchemaType(props.schemaType).map(
58
- (field) => field.type.name
59
- )
60
- const hasInternationalizedArray = flatFieldTypeNames.some((name) =>
61
- name.startsWith('internationalizedArray')
62
- )
63
-
64
- if (!hasInternationalizedArray) {
65
- return props.renderDefault(props)
66
- }
67
-
68
- return (
69
- <InternationalizedArrayProvider
70
- {...props}
71
- internationalizedArray={pluginConfig}
72
- />
73
- )
74
- },
75
- },
76
- },
77
- // Register custom schema types for the outer array and the inner object
78
- schema: {
79
- types: [
80
- ...fieldTypes.map((type) =>
81
- array({type, apiVersion, select, languages, defaultLanguages})
82
- ),
83
- ...fieldTypes.map((type) => object({type})),
84
- ],
85
- },
86
- }
87
- })