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,286 +0,0 @@
1
- import {AddIcon} from '@sanity/icons'
2
- import {useLanguageFilterStudioContext} from '@sanity/language-filter'
3
- import {Button, Card, Stack, Text, useToast} from '@sanity/ui'
4
- import type React from 'react'
5
- import {useCallback, useEffect, useMemo} from 'react'
6
- import {
7
- type ArrayOfObjectsInputProps,
8
- ArrayOfObjectsItem,
9
- type ArraySchemaType,
10
- MemberItemError,
11
- set,
12
- setIfMissing,
13
- useFormValue,
14
- } from 'sanity'
15
- import {useDocumentPane} from 'sanity/structure'
16
-
17
- import type {Value} from '../types'
18
- import {checkAllLanguagesArePresent} from '../utils/checkAllLanguagesArePresent'
19
- import {createAddAllTitle} from '../utils/createAddAllTitle'
20
- import {createAddLanguagePatches} from '../utils/createAddLanguagePatches'
21
- import AddButtons from './AddButtons'
22
- import Feedback from './Feedback'
23
- import {useInternationalizedArrayContext} from './InternationalizedArrayContext'
24
-
25
- export type InternationalizedArrayProps = ArrayOfObjectsInputProps<
26
- Value,
27
- ArraySchemaType
28
- >
29
-
30
- export default function InternationalizedArray(
31
- props: InternationalizedArrayProps
32
- ): React.ReactElement {
33
- const {
34
- members,
35
- value,
36
- schemaType,
37
- onChange,
38
- readOnly: documentReadOnly,
39
- } = props
40
-
41
- const readOnly =
42
- typeof schemaType.readOnly === 'boolean' ? schemaType.readOnly : false
43
- const toast = useToast()
44
-
45
- const {
46
- languages,
47
- filteredLanguages,
48
- defaultLanguages,
49
- buttonAddAll,
50
- buttonLocations,
51
- } = useInternationalizedArrayContext()
52
-
53
- // Support updating the UI if languageFilter is installed
54
- const {selectedLanguageIds, options: languageFilterOptions} =
55
- useLanguageFilterStudioContext()
56
- const documentType = useFormValue(['_type'])
57
- const languageFilterEnabled =
58
- typeof documentType === 'string' &&
59
- languageFilterOptions.documentTypes.includes(documentType)
60
-
61
- const filteredMembers = useMemo(
62
- () =>
63
- languageFilterEnabled
64
- ? members.filter((member) => {
65
- // This member is the outer object created by the plugin
66
- // Satisfy TS
67
- if (member.kind !== 'item') {
68
- return false
69
- }
70
-
71
- // This is the inner "value" field member created by this plugin
72
- const valueMember = member.item.members[0]
73
-
74
- // Satisfy TS
75
- if (valueMember.kind !== 'field') {
76
- return false
77
- }
78
-
79
- return languageFilterOptions.filterField(
80
- member.item.schemaType,
81
- valueMember,
82
- selectedLanguageIds
83
- )
84
- })
85
- : members,
86
- [languageFilterEnabled, members, languageFilterOptions, selectedLanguageIds]
87
- )
88
-
89
- const handleAddLanguage = useCallback(
90
- async (
91
- param?: React.MouseEvent<HTMLButtonElement, MouseEvent> | string[]
92
- ) => {
93
- if (!filteredLanguages?.length) {
94
- return
95
- }
96
-
97
- const addLanguageKeys: string[] = Array.isArray(param)
98
- ? param
99
- : ([param?.currentTarget?.value].filter(Boolean) as string[])
100
-
101
- const patches = createAddLanguagePatches({
102
- addLanguageKeys,
103
- schemaType,
104
- languages,
105
- filteredLanguages,
106
- value,
107
- })
108
-
109
- onChange([setIfMissing([]), ...patches])
110
- },
111
- [filteredLanguages, languages, onChange, schemaType, value]
112
- )
113
-
114
- const {isDeleting} = useDocumentPane()
115
-
116
- const addedLanguages = members.map(({key}) => key)
117
- const hasAddedDefaultLanguages = defaultLanguages
118
- .filter((language) => languages.find((l) => l.id === language))
119
- .every((language) => addedLanguages.includes(language))
120
-
121
- useEffect(() => {
122
- if (!isDeleting && !hasAddedDefaultLanguages) {
123
- const languagesToAdd = defaultLanguages
124
- .filter((language) => !addedLanguages.includes(language))
125
- .filter((language) => languages.find((l) => l.id === language))
126
- // Account for strict mode by scheduling the update
127
- const timeout = setTimeout(() => {
128
- if (!documentReadOnly) handleAddLanguage(languagesToAdd)
129
- })
130
- return () => clearTimeout(timeout)
131
- }
132
- return undefined
133
- }, [
134
- isDeleting,
135
- hasAddedDefaultLanguages,
136
- handleAddLanguage,
137
- defaultLanguages,
138
- addedLanguages,
139
- languages,
140
- documentReadOnly,
141
- ])
142
-
143
- // NOTE: This is reordering and re-setting the whole array, it could be surgical
144
- const handleRestoreOrder = useCallback(() => {
145
- if (!value?.length || !languages?.length) {
146
- return
147
- }
148
-
149
- // Create a new value array in the correct order
150
- // This would also strip out values that don't have a language as the key
151
- const updatedValue = value
152
- .reduce((acc, v) => {
153
- const newIndex = languages.findIndex((l) => l.id === v?._key)
154
-
155
- if (newIndex > -1) {
156
- acc[newIndex] = v
157
- }
158
-
159
- return acc
160
- }, [] as Value[])
161
- .filter(Boolean)
162
-
163
- if (value?.length !== updatedValue.length) {
164
- toast.push({
165
- title: 'There was an error reordering languages',
166
- status: 'warning',
167
- })
168
- }
169
-
170
- onChange(set(updatedValue))
171
- }, [toast, languages, onChange, value])
172
-
173
- const allKeysAreLanguages = useMemo(() => {
174
- if (!value?.length || !languages?.length) {
175
- return true
176
- }
177
-
178
- return value?.every((v) => languages.find((l) => l?.id === v?._key))
179
- }, [value, languages])
180
-
181
- // Check languages are in the correct order
182
- const languagesInUse = useMemo(
183
- () =>
184
- languages && languages.length > 1
185
- ? languages.filter((l) => value?.find((v) => v._key === l.id))
186
- : [],
187
- [languages, value]
188
- )
189
-
190
- const languagesOutOfOrder = useMemo(() => {
191
- if (!value?.length || !languagesInUse.length) {
192
- return []
193
- }
194
-
195
- return value
196
- .map((v, vIndex) =>
197
- vIndex === languagesInUse.findIndex((l) => l.id === v._key) ? null : v
198
- )
199
- .filter(Boolean)
200
- }, [value, languagesInUse])
201
-
202
- const languagesAreValid = useMemo(
203
- () =>
204
- !languages?.length ||
205
- (languages?.length && languages.every((item) => item.id && item.title)),
206
- [languages]
207
- )
208
-
209
- // Automatically restore order of fields
210
- useEffect(() => {
211
- if (languagesOutOfOrder.length > 0 && allKeysAreLanguages) {
212
- handleRestoreOrder()
213
- }
214
- }, [languagesOutOfOrder, allKeysAreLanguages, handleRestoreOrder])
215
-
216
- // compare value keys with possible languages
217
- const allLanguagesArePresent = useMemo(
218
- () => checkAllLanguagesArePresent(filteredLanguages, value),
219
- [filteredLanguages, value]
220
- )
221
-
222
- if (!languagesAreValid) {
223
- return <Feedback />
224
- }
225
-
226
- const addButtonsAreVisible =
227
- // Plugin was configured to display buttons here (default!)
228
- buttonLocations.includes('field') &&
229
- // There's at least one language visible
230
- filteredLanguages?.length > 0 &&
231
- // Not every language has a value yet
232
- !allLanguagesArePresent
233
- const fieldHasMembers = members?.length > 0
234
-
235
- return (
236
- <Stack space={2}>
237
- {fieldHasMembers ? (
238
- <>
239
- {filteredMembers.map((member) => {
240
- if (member.kind === 'item') {
241
- return (
242
- <ArrayOfObjectsItem
243
- {...props}
244
- key={member.key}
245
- member={member}
246
- />
247
- )
248
- }
249
-
250
- return <MemberItemError key={member.key} member={member} />
251
- })}
252
- </>
253
- ) : null}
254
-
255
- {/* Give some feedback in the UI so the field doesn't look "missing" */}
256
- {!addButtonsAreVisible && !fieldHasMembers ? (
257
- <Card border tone="transparent" padding={3} radius={2}>
258
- <Text size={1}>
259
- This internationalized field currently has no translations.
260
- </Text>
261
- </Card>
262
- ) : null}
263
-
264
- {addButtonsAreVisible ? (
265
- <Stack space={2}>
266
- <AddButtons
267
- languages={filteredLanguages}
268
- value={value}
269
- readOnly={readOnly}
270
- onClick={handleAddLanguage}
271
- />
272
- {buttonAddAll ? (
273
- <Button
274
- tone="primary"
275
- mode="ghost"
276
- disabled={readOnly || allLanguagesArePresent}
277
- icon={AddIcon}
278
- text={createAddAllTitle(value, filteredLanguages)}
279
- onClick={handleAddLanguage}
280
- />
281
- ) : null}
282
- </Stack>
283
- ) : null}
284
- </Stack>
285
- )
286
- }
@@ -1,136 +0,0 @@
1
- import {useLanguageFilterStudioContext} from '@sanity/language-filter'
2
- import {Stack} from '@sanity/ui'
3
- import equal from 'fast-deep-equal'
4
- import type React from 'react'
5
- import {createContext, useContext, useDeferredValue, useMemo} from 'react'
6
- import {type ObjectInputProps, useClient, useWorkspace} from 'sanity'
7
- import {useDocumentPane} from 'sanity/structure'
8
- import {suspend} from 'suspend-react'
9
-
10
- import {createCacheKey, setFunctionCache} from '../cache'
11
- import {CONFIG_DEFAULT} from '../constants'
12
- import type {Language, PluginConfig} from '../types'
13
- import DocumentAddButtons from './DocumentAddButtons'
14
- import {getSelectedValue} from './getSelectedValue'
15
-
16
- // This provider makes the plugin config available to all components in the document form
17
- // But with languages resolved and filtered languages updated base on @sanity/language-filter
18
-
19
- type InternationalizedArrayContextProps = Required<PluginConfig> & {
20
- languages: Language[]
21
- filteredLanguages: Language[]
22
- }
23
-
24
- export const InternationalizedArrayContext =
25
- createContext<InternationalizedArrayContextProps>({
26
- ...CONFIG_DEFAULT,
27
- languages: [],
28
- filteredLanguages: [],
29
- })
30
-
31
- export function useInternationalizedArrayContext(): InternationalizedArrayContextProps {
32
- return useContext(InternationalizedArrayContext)
33
- }
34
-
35
- type InternationalizedArrayProviderProps = ObjectInputProps & {
36
- internationalizedArray: Required<PluginConfig>
37
- }
38
-
39
- export function InternationalizedArrayProvider(
40
- props: InternationalizedArrayProviderProps
41
- ): React.ReactElement {
42
- const {internationalizedArray} = props
43
-
44
- const client = useClient({apiVersion: internationalizedArray.apiVersion})
45
- const workspace = useWorkspace()
46
- const {formState} = useDocumentPane()
47
- const deferredDocument = useDeferredValue(formState?.value)
48
- const selectedValue = useMemo(
49
- () => getSelectedValue(internationalizedArray.select, deferredDocument),
50
- [internationalizedArray.select, deferredDocument]
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
-
74
- // Fetch or return languages
75
- const languages = Array.isArray(internationalizedArray.languages)
76
- ? internationalizedArray.languages
77
- : suspend(
78
- // eslint-disable-next-line require-await
79
- async () => {
80
- if (typeof internationalizedArray.languages === 'function') {
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
93
- }
94
- return internationalizedArray.languages
95
- },
96
- cacheKey,
97
- {equal}
98
- )
99
-
100
- // Filter out some languages if language filter is enabled
101
- const {selectedLanguageIds, options: languageFilterOptions} =
102
- useLanguageFilterStudioContext()
103
-
104
- const filteredLanguages = useMemo(() => {
105
- const documentType = deferredDocument ? deferredDocument._type : undefined
106
- const languageFilterEnabled =
107
- typeof documentType === 'string' &&
108
- languageFilterOptions.documentTypes.includes(documentType)
109
-
110
- return languageFilterEnabled
111
- ? languages.filter((language) =>
112
- selectedLanguageIds.includes(language.id)
113
- )
114
- : languages
115
- }, [deferredDocument, languageFilterOptions, languages, selectedLanguageIds])
116
-
117
- const showDocumentButtons =
118
- internationalizedArray.buttonLocations.includes('document')
119
- const context = useMemo(
120
- () => ({...internationalizedArray, languages, filteredLanguages}),
121
- [filteredLanguages, internationalizedArray, languages]
122
- )
123
-
124
- return (
125
- <InternationalizedArrayContext.Provider value={context}>
126
- {showDocumentButtons ? (
127
- <Stack space={5}>
128
- <DocumentAddButtons value={props.value} />
129
- {props.renderDefault(props)}
130
- </Stack>
131
- ) : (
132
- props.renderDefault(props)
133
- )}
134
- </InternationalizedArrayContext.Provider>
135
- )
136
- }
@@ -1,57 +0,0 @@
1
- import type {ReactNode} from 'react'
2
- import {useMemo} from 'react'
3
- import {type FieldProps} from 'sanity'
4
-
5
- import {useInternationalizedArrayContext} from './InternationalizedArrayContext'
6
-
7
- export default function InternationalizedField(props: FieldProps): ReactNode {
8
- const {languages} = useInternationalizedArrayContext()
9
-
10
- // hide titles for 'value' fields within valid language entries
11
- const customProps = useMemo(() => {
12
- const pathSegment = props.path.slice(0, -1)[1]
13
- const languageId =
14
- typeof pathSegment === 'object' && '_key' in pathSegment
15
- ? pathSegment._key
16
- : undefined
17
- const hasValidLanguageId = languageId
18
- ? languages.some((l) => l.id === languageId)
19
- : false
20
- const shouldHideTitle =
21
- props.title?.toLowerCase() === 'value' && hasValidLanguageId
22
-
23
- return {
24
- ...props,
25
- title: shouldHideTitle ? '' : props.title,
26
- }
27
- }, [props, languages])
28
-
29
- if (!customProps.schemaType.name.startsWith('internationalizedArray')) {
30
- return customProps.renderDefault(customProps)
31
- }
32
-
33
- // Show reference field selector if there's a value
34
- if (customProps.schemaType.name === 'reference' && customProps.value) {
35
- return customProps.renderDefault({
36
- ...customProps,
37
- title: '',
38
- level: 0, // Reset the level to avoid nested styling
39
- })
40
- }
41
-
42
- // For basic field types, we can use children to keep the simple input
43
- if (
44
- customProps.schemaType.name === 'string' ||
45
- customProps.schemaType.name === 'number' ||
46
- customProps.schemaType.name === 'text'
47
- ) {
48
- return customProps.children
49
- }
50
-
51
- // For complex fields (like markdown), we need to use renderDefault
52
- // to get all the field's functionality
53
- return customProps.renderDefault({
54
- ...customProps,
55
- level: 0, // Reset the level to avoid nested styling
56
- })
57
- }