@sanity/assist 1.2.15 → 2.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 (52) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +551 -30
  3. package/dist/index.cjs.mjs +1 -0
  4. package/dist/index.d.ts +253 -11
  5. package/dist/index.esm.js +2405 -392
  6. package/dist/index.esm.js.map +1 -1
  7. package/dist/index.js +2399 -385
  8. package/dist/index.js.map +1 -1
  9. package/package.json +15 -14
  10. package/src/_lib/form/DocumentForm.tsx +3 -2
  11. package/src/_lib/form/constants.ts +1 -0
  12. package/src/assistDocument/AssistDocumentInput.tsx +24 -4
  13. package/src/assistDocument/RequestRunInstructionProvider.tsx +37 -21
  14. package/src/assistDocument/components/instruction/InstructionInput.tsx +5 -4
  15. package/src/assistDocument/components/instruction/InstructionOutputField.tsx +45 -0
  16. package/src/assistDocument/components/instruction/InstructionOutputInput.tsx +205 -0
  17. package/src/assistDocument/hooks/useStudioAssistDocument.ts +5 -32
  18. package/src/assistFormComponents/AssistField.tsx +11 -5
  19. package/src/assistFormComponents/AssistFormBlock.tsx +2 -3
  20. package/src/assistFormComponents/validation/listItem.tsx +2 -2
  21. package/src/assistInspector/AssistInspector.tsx +6 -0
  22. package/src/assistInspector/FieldAutocomplete.tsx +1 -0
  23. package/src/assistInspector/InstructionTaskHistoryButton.tsx +2 -3
  24. package/src/assistInspector/helpers.ts +9 -11
  25. package/src/assistLayout/AssistLayout.tsx +9 -9
  26. package/src/components/ImageContext.tsx +19 -9
  27. package/src/components/SafeValueInput.tsx +4 -1
  28. package/src/fieldActions/assistFieldActions.tsx +42 -13
  29. package/src/fieldActions/generateCaptionActions.tsx +2 -2
  30. package/src/fieldActions/generateImageActions.tsx +57 -0
  31. package/src/helpers/assistSupported.ts +10 -16
  32. package/src/helpers/conditionalMembers.test.ts +200 -0
  33. package/src/helpers/conditionalMembers.ts +127 -0
  34. package/src/helpers/typeUtils.ts +19 -5
  35. package/src/index.ts +3 -0
  36. package/src/plugin.tsx +14 -5
  37. package/src/presence/AssistAvatar.tsx +1 -1
  38. package/src/schemas/assistDocumentSchema.tsx +40 -1
  39. package/src/schemas/serialize/serializeSchema.test.ts +239 -8
  40. package/src/schemas/serialize/serializeSchema.ts +77 -10
  41. package/src/schemas/typeDefExtensions.ts +89 -5
  42. package/src/translate/FieldTranslationProvider.tsx +360 -0
  43. package/src/translate/getLanguageParams.ts +26 -0
  44. package/src/translate/languageStore.ts +18 -0
  45. package/src/translate/paths.test.ts +133 -0
  46. package/src/translate/paths.ts +175 -0
  47. package/src/translate/translateActions.tsx +188 -0
  48. package/src/translate/types.ts +160 -0
  49. package/src/types.ts +33 -12
  50. package/src/useApiClient.ts +130 -2
  51. package/src/assistLayout/AlphaMigration.tsx +0 -310
  52. package/src/legacy-types.ts +0 -72
@@ -1,21 +1,24 @@
1
1
  import {
2
2
  ArraySchemaType,
3
3
  ImageOptions,
4
+ isArraySchemaType,
4
5
  ObjectSchemaType,
6
+ ReferenceOptions,
5
7
  ReferenceSchemaType,
6
8
  Schema,
7
9
  SchemaType,
8
10
  typed,
9
11
  } from 'sanity'
10
12
  import {
13
+ assistSchemaIdPrefix,
11
14
  assistSerializedFieldTypeName,
12
15
  assistSerializedTypeName,
13
16
  SerializedSchemaMember,
14
17
  SerializedSchemaType,
15
- assistSchemaIdPrefix,
16
18
  } from '../../types'
17
19
  import {hiddenTypes} from './schemaUtils'
18
20
  import {isAssistSupported} from '../../helpers/assistSupported'
21
+ import {isType} from '../../helpers/typeUtils'
19
22
 
20
23
  interface Options {
21
24
  leanFormat?: boolean
@@ -31,7 +34,6 @@ export function serializeSchema(schema: Schema, options?: Options): SerializedSc
31
34
  .filter((t): t is SchemaType => !!t)
32
35
  // because a field can override exclude at the type level, we have to also serialize excluded types
33
36
  // so don't do this: .filter((t) => isAssistSupported(t))
34
- .filter((t) => !t.hidden && !t.readOnly)
35
37
  .map((t) => getSchemaStub(t, schema, options))
36
38
  .filter((t) => {
37
39
  if ('to' in t && t.to && !t.to.length) {
@@ -78,13 +80,12 @@ function getBaseFields(
78
80
  typeName: string,
79
81
  options: Options | undefined
80
82
  ) {
81
- const imagePromptField = (type.options as ImageOptions)?.imagePromptField
83
+ const schemaOptions: SerializedSchemaType['options'] = removeUndef({
84
+ imagePromptField: (type.options as ImageOptions)?.aiAssist?.imageInstructionField,
85
+ embeddingsIndex: (type.options as ReferenceOptions)?.aiAssist?.embeddingsIndex,
86
+ })
82
87
  return removeUndef({
83
- options: imagePromptField
84
- ? {
85
- imagePromptField: imagePromptField,
86
- }
87
- : undefined,
88
+ options: Object.keys(schemaOptions).length ? schemaOptions : undefined,
88
89
  values: Array.isArray(type?.options?.list)
89
90
  ? type?.options?.list.map((v: string | {value: string; title: string}) =>
90
91
  typeof v === 'string' ? v : v.value ?? `${v.title}`
@@ -99,6 +100,22 @@ function getBaseFields(
99
100
  'fields' in type && inlineTypes.includes(typeName)
100
101
  ? serializeFields(schema, type, options)
101
102
  : undefined,
103
+ annotations:
104
+ typeName === 'block' && 'fields' in type
105
+ ? serializeAnnotations(type, schema, options)
106
+ : undefined,
107
+ inlineOf:
108
+ typeName === 'block' && 'fields' in type
109
+ ? serializeInlineOf(type, schema, options)
110
+ : undefined,
111
+ hidden:
112
+ typeof type.hidden === 'function' ? ('function' as const) : type.hidden ? true : undefined,
113
+ readOnly:
114
+ typeof type.readOnly === 'function'
115
+ ? ('function' as const)
116
+ : type.readOnly
117
+ ? true
118
+ : undefined,
102
119
  })
103
120
  }
104
121
 
@@ -107,10 +124,27 @@ function serializeFields(
107
124
  schemaType: ObjectSchemaType,
108
125
  options: Options | undefined
109
126
  ) {
110
- return schemaType.fields
127
+ const fields = schemaType.fieldsets
128
+ ? schemaType.fieldsets.flatMap((fs) =>
129
+ fs.single
130
+ ? fs.field
131
+ : fs.fields.map((f) => ({
132
+ ...f,
133
+ type: {
134
+ ...f.type,
135
+ // if fieldset is (conditionally) hidden, the field must be considered the same way
136
+ // ie, if the field does not show up in conditionalMembers, it is hidden
137
+ // regardless of weather or not it is the field function or the fieldset function that hides it
138
+ hidden:
139
+ typeof fs.hidden === 'function' ? fs.hidden : fs.hidden ? true : f.type.hidden,
140
+ },
141
+ }))
142
+ )
143
+ : schemaType.fields
144
+
145
+ return fields
111
146
  .filter((f) => !['sanity.imageHotspot', 'sanity.imageCrop'].includes(f.type?.name ?? ''))
112
147
  .filter((f) => isAssistSupported(f.type))
113
- .filter((f) => !f.type.hidden && !f.type.readOnly)
114
148
  .map((field) => serializeMember(schema, field.type, field.name, options))
115
149
  }
116
150
 
@@ -131,6 +165,39 @@ function serializeMember(
131
165
  })
132
166
  }
133
167
 
168
+ function serializeInlineOf(
169
+ blockSchemaType: ObjectSchemaType,
170
+ schema: Schema,
171
+ options: Options | undefined
172
+ ): SerializedSchemaMember[] | undefined {
173
+ const childrenField = blockSchemaType.fields.find((f) => f.name === 'children')
174
+ const childrenType = childrenField?.type
175
+ if (!childrenType || !isArraySchemaType(childrenType)) {
176
+ return undefined
177
+ }
178
+ return arrayOf(
179
+ {
180
+ ...childrenType,
181
+ of: childrenType.of.filter((t) => !isType(t, 'span')),
182
+ },
183
+ schema,
184
+ options
185
+ )
186
+ }
187
+
188
+ function serializeAnnotations(
189
+ blockSchemaType: ObjectSchemaType,
190
+ schema: Schema,
191
+ options: Options | undefined
192
+ ): SerializedSchemaMember[] | undefined {
193
+ const markDefs = blockSchemaType.fields.find((f) => f.name === 'markDefs')
194
+ const marksType = markDefs?.type
195
+ if (!marksType || !isArraySchemaType(marksType)) {
196
+ return undefined
197
+ }
198
+ return arrayOf(marksType, schema, options)
199
+ }
200
+
134
201
  function arrayOf(
135
202
  arrayType: ArraySchemaType,
136
203
  schema: Schema,
@@ -1,8 +1,14 @@
1
1
  /* eslint-disable no-unused-vars */
2
2
  export interface AssistOptions {
3
- aiWritingAssistance?: {
3
+ aiAssist?: {
4
4
  /** Set to true to disable assistance for this field or type */
5
5
  exclude?: boolean
6
+
7
+ /**
8
+ * Set to true to add translation field-action to the field.
9
+ * Only has an effect in document types configured for document or field level translations.
10
+ */
11
+ translateAction?: boolean
6
12
  }
7
13
  }
8
14
 
@@ -16,13 +22,91 @@ declare module 'sanity' {
16
22
  interface DocumentOptions extends AssistOptions {}
17
23
  interface FileOptions extends AssistOptions {}
18
24
  interface GeopointOptions extends AssistOptions {}
19
- interface ImageOptions extends AssistOptions {
20
- imagePromptField?: string
21
- captionField?: string
25
+ interface ImageOptions {
26
+ aiAssist?: AssistOptions['aiAssist'] & {
27
+ /**
28
+ * When set, an image will be created whenever the `imageInstructionField` is written to by
29
+ * an AI Assist instruction.
30
+ *
31
+ * The value output by AI Assist will be use as an image prompt for an generative image AI.
32
+ *
33
+ * This means that instructions directly for the field or instructions that visit the field when running,
34
+ * will result in the image being changed.
35
+ *
36
+ * `imageInstructionField` must be a child-path relative to the image field, ie:
37
+ * * field
38
+ * * path.to.field
39
+ *
40
+ * ### Example
41
+ * ```ts
42
+ * defineType({
43
+ * type: 'image',
44
+ * name: 'articleImage',
45
+ * fields: [
46
+ * defineField({
47
+ * type: 'text',
48
+ * name: 'imagePrompt',
49
+ * title: 'Image prompt',
50
+ * rows: 2,
51
+ * }),
52
+ * ],
53
+ * options: {
54
+ * aiAssist: {
55
+ * imageInstructionField: 'imagePrompt',
56
+ * }
57
+ * },
58
+ * })
59
+ * ```
60
+ */
61
+ imageInstructionField?: string
62
+
63
+ /**
64
+ * When set, an image description will be automatically created for the image.
65
+ *
66
+ * `imageDescriptionField` must be a child-path relative to the image field, ie:
67
+ * * field
68
+ * * path.to.field
69
+ *
70
+ * Whenever the image asset for the field is changed in the Studio,
71
+ * an image description is generated and set into the `imageDescriptionField`.
72
+ *
73
+ * ### Example
74
+ * ```ts
75
+ * defineType({
76
+ * type: 'image',
77
+ * name: 'articleImage',
78
+ * fields: [
79
+ * defineField({
80
+ * type: 'string',
81
+ * name: 'altText',
82
+ * title: 'Alt text',
83
+ * }),
84
+ * ],
85
+ * options: {
86
+ * aiAssist: {
87
+ * imageDescriptionField: 'altText',
88
+ * }
89
+ * },
90
+ * })
91
+ * ```
92
+ */
93
+ imageDescriptionField?: string
94
+ }
22
95
  }
23
96
  interface NumberOptions extends AssistOptions {}
24
97
  interface ObjectOptions extends AssistOptions {}
25
- interface ReferenceBaseOptions extends AssistOptions {}
98
+ interface ReferenceBaseOptions {
99
+ aiAssist?: {
100
+ /** Set to true to disable assistance for this field or type */
101
+ exclude?: boolean
102
+
103
+ /**
104
+ * When set, the reference field will allow instructions to be added to it.
105
+ * Should be the name of the embeddings-index where assist will look for contextually relevant documents
106
+ * */
107
+ embeddingsIndex?: string
108
+ }
109
+ }
26
110
  interface SlugOptions extends AssistOptions {}
27
111
  interface StringOptions extends AssistOptions {}
28
112
  interface TextOptions extends AssistOptions {}
@@ -0,0 +1,360 @@
1
+ import {
2
+ createContext,
3
+ PropsWithChildren,
4
+ useCallback,
5
+ useContext,
6
+ useId,
7
+ useMemo,
8
+ useState,
9
+ } from 'react'
10
+ import {ObjectSchemaType, Path, pathToString, SanityDocumentLike, useClient} from 'sanity'
11
+ import {useAiAssistanceConfig} from '../assistLayout/AiAssistanceConfigContext'
12
+ import {useApiClient, useTranslate} from '../useApiClient'
13
+ import {Box, Button, Checkbox, Dialog, Flex, Radio, Spinner, Stack, Text, Tooltip} from '@sanity/ui'
14
+ import {
15
+ defaultLanguageOutputs,
16
+ FieldLanguageMap,
17
+ getDocumentMembersFlat,
18
+ getFieldLanguageMap,
19
+ } from './paths'
20
+ import {PlayIcon} from '@sanity/icons'
21
+ import {Language} from './types'
22
+ import {getLanguageParams} from './getLanguageParams'
23
+ import {getPreferredToFieldLanguages, setPreferredToFieldLanguages} from './languageStore'
24
+ import {ConditionalMemberState} from '../helpers/conditionalMembers'
25
+
26
+ interface FieldTranslationParams {
27
+ document: SanityDocumentLike
28
+ documentSchema: ObjectSchemaType
29
+ translatePath: Path
30
+ conditionalMembers: ConditionalMemberState[]
31
+ }
32
+
33
+ export interface FieldTranslationContextValue {
34
+ openFieldTranslation: (args: FieldTranslationParams) => void
35
+ translationLoading: boolean
36
+ }
37
+
38
+ export const FieldTranslationContext = createContext<FieldTranslationContextValue>({
39
+ openFieldTranslation: () => {},
40
+ translationLoading: false,
41
+ })
42
+
43
+ export function useFieldTranslation() {
44
+ return useContext(FieldTranslationContext)
45
+ }
46
+
47
+ function hasValuesToTranslate(
48
+ fieldLanguageMaps: FieldLanguageMap[] | undefined,
49
+ fromLanguage: Language | undefined,
50
+ basePath: Path
51
+ ) {
52
+ return fieldLanguageMaps?.some(
53
+ (map) =>
54
+ map.inputLanguageId === fromLanguage?.id &&
55
+ map.inputPath &&
56
+ pathToString(map.inputPath).startsWith(pathToString(basePath))
57
+ )
58
+ }
59
+
60
+ export function FieldTranslationProvider(props: PropsWithChildren<{}>) {
61
+ const {config: assistConfig} = useAiAssistanceConfig()
62
+ const apiClient = useApiClient(assistConfig.__customApiClient)
63
+ const config = assistConfig.translate?.field
64
+ const {translate: runTranslate} = useTranslate(apiClient)
65
+
66
+ const [dialogOpen, setDialogOpen] = useState(false)
67
+
68
+ const [fieldTranslationParams, setFieldTranslationParams] = useState<
69
+ FieldTranslationParams | undefined
70
+ >()
71
+ const [languages, setLanguages] = useState<Language[] | undefined>()
72
+ const [fromLanguage, setFromLanguage] = useState<Language | undefined>(undefined)
73
+ const [toLanguages, setToLanguages] = useState<Language[] | undefined>(undefined)
74
+ const [fieldLanguageMaps, setFieldLanguageMaps] = useState<FieldLanguageMap[] | undefined>()
75
+
76
+ const close = useCallback(() => {
77
+ setDialogOpen(false)
78
+ setLanguages(undefined)
79
+ setFieldTranslationParams(undefined)
80
+ }, [])
81
+ const languageClient = useClient({apiVersion: config?.apiVersion ?? '2022-11-27'})
82
+ const documentId = fieldTranslationParams?.document?._id
83
+ const id = useId()
84
+
85
+ const selectFromLanguage = useCallback(
86
+ (
87
+ from: Language,
88
+ languages: Language[] | undefined,
89
+ params: FieldTranslationParams | undefined
90
+ ) => {
91
+ const {document, documentSchema} = params ?? {}
92
+ setFromLanguage(from)
93
+ if (!document || !documentSchema || !params || !languages) {
94
+ setFieldLanguageMaps(undefined)
95
+ return
96
+ }
97
+
98
+ const preferred = getPreferredToFieldLanguages(from.id)
99
+ const allToLanguages = languages.filter((l) => l.id !== from?.id)
100
+ const filteredToLanguages = allToLanguages.filter(
101
+ (l) => !preferred.length || preferred.includes(l.id)
102
+ )
103
+
104
+ setToLanguages(filteredToLanguages)
105
+ const fromId = from?.id
106
+ const allToIds = allToLanguages?.map((l) => l.id) ?? []
107
+ const docMembers = getDocumentMembersFlat(document, documentSchema)
108
+ if (fromId && allToIds?.length) {
109
+ const transMap = getFieldLanguageMap(
110
+ documentSchema,
111
+ docMembers,
112
+ fromId,
113
+ allToIds.filter((toId) => fromId !== toId),
114
+ config?.translationOutputs ?? defaultLanguageOutputs
115
+ )
116
+ setFieldLanguageMaps(transMap)
117
+ } else {
118
+ setFieldLanguageMaps(undefined)
119
+ }
120
+ },
121
+ [config]
122
+ )
123
+
124
+ const toggleToLanguage = useCallback(
125
+ (
126
+ toggledLang: Language,
127
+ toLanguages: Language[] | undefined,
128
+ languages: Language[] | undefined
129
+ ) => {
130
+ if (!languages || !fromLanguage) {
131
+ return
132
+ }
133
+ const wasSelected = !!toLanguages?.find((l) => l.id === toggledLang.id)
134
+ const newToLangs = languages.filter(
135
+ (anyLang) =>
136
+ !!toLanguages?.find(
137
+ (selectedLang) => toggledLang.id !== selectedLang.id && selectedLang.id === anyLang.id
138
+ ) ||
139
+ (toggledLang.id === anyLang.id && !wasSelected)
140
+ )
141
+ setToLanguages(newToLangs)
142
+ setPreferredToFieldLanguages(
143
+ fromLanguage.id,
144
+ newToLangs.map((l) => l.id)
145
+ )
146
+ },
147
+ [fromLanguage]
148
+ )
149
+
150
+ const openFieldTranslation = useCallback(
151
+ async (params: FieldTranslationParams) => {
152
+ setDialogOpen(true)
153
+ const languageParams = getLanguageParams(config?.selectLanguageParams, params.document)
154
+ const languages: Language[] | undefined = await (typeof config?.languages === 'function'
155
+ ? config?.languages(languageClient, languageParams)
156
+ : Promise.resolve(config?.languages))
157
+ setLanguages(languages)
158
+ setFieldTranslationParams(params)
159
+ const fromLanguage = languages?.[0]
160
+ if (fromLanguage) {
161
+ selectFromLanguage(fromLanguage, languages, params)
162
+ } else {
163
+ console.error('No languages available for selected language params', languageParams)
164
+ }
165
+ },
166
+ [selectFromLanguage, config, languageClient]
167
+ )
168
+
169
+ const contextValue: FieldTranslationContextValue = useMemo(() => {
170
+ return {
171
+ openFieldTranslation,
172
+ translationLoading: false,
173
+ }
174
+ }, [openFieldTranslation])
175
+
176
+ const runDisabled =
177
+ !fromLanguage ||
178
+ !toLanguages?.length ||
179
+ !fieldLanguageMaps?.length ||
180
+ !documentId ||
181
+ !hasValuesToTranslate(fieldLanguageMaps, fromLanguage, fieldTranslationParams.translatePath)
182
+
183
+ const onRunTranslation = useCallback(() => {
184
+ const translatePath = fieldTranslationParams?.translatePath
185
+ if (fieldLanguageMaps && documentId && translatePath) {
186
+ runTranslate({
187
+ documentId,
188
+ translatePath: translatePath,
189
+ fieldLanguageMap: fieldLanguageMaps.map((map) => ({
190
+ ...map,
191
+ // eslint-disable-next-line max-nested-callbacks
192
+ outputs: map.outputs.filter((out) => !!toLanguages?.find((l) => l.id === out.id)),
193
+ })),
194
+ conditionalMembers: fieldTranslationParams?.conditionalMembers,
195
+ })
196
+ }
197
+ close()
198
+ }, [
199
+ fieldLanguageMaps,
200
+ documentId,
201
+ runTranslate,
202
+ close,
203
+ toLanguages,
204
+ fieldTranslationParams?.translatePath,
205
+ fieldTranslationParams?.conditionalMembers,
206
+ ])
207
+
208
+ const runButton = (
209
+ <Button
210
+ text={`Translate`}
211
+ tone="primary"
212
+ icon={PlayIcon}
213
+ style={{width: '100%'}}
214
+ disabled={runDisabled}
215
+ onClick={onRunTranslation}
216
+ />
217
+ )
218
+
219
+ return (
220
+ <FieldTranslationContext.Provider value={contextValue}>
221
+ {dialogOpen ? (
222
+ <Dialog
223
+ id={id}
224
+ width={1}
225
+ open={dialogOpen}
226
+ onClose={close}
227
+ header="Translate fields"
228
+ footer={
229
+ <Flex justify="space-between" padding={2} flex={1}>
230
+ {runDisabled ? (
231
+ <Tooltip
232
+ content={
233
+ <Flex padding={2}>
234
+ <Text>There is nothing to translate in the selected from-language.</Text>
235
+ </Flex>
236
+ }
237
+ placement="top"
238
+ >
239
+ <Flex flex={1}>{runButton}</Flex>
240
+ </Tooltip>
241
+ ) : (
242
+ runButton
243
+ )}
244
+ </Flex>
245
+ }
246
+ >
247
+ {languages ? (
248
+ <Flex padding={4} gap={5} align="flex-start" justify="center">
249
+ <Stack space={2}>
250
+ <Box marginBottom={2}>
251
+ <Text weight="semibold">From</Text>
252
+ </Box>
253
+ {languages?.map((radioLanguage) => (
254
+ <FromLanguageRadio
255
+ key={radioLanguage.id}
256
+ {...{
257
+ radioLanguage,
258
+ fromLanguage,
259
+ selectFromLanguage,
260
+ languages,
261
+ fieldTranslationParams,
262
+ }}
263
+ />
264
+ ))}
265
+ </Stack>
266
+
267
+ <Stack space={2}>
268
+ <Box marginBottom={2}>
269
+ <Text weight="semibold">To</Text>
270
+ </Box>
271
+ {languages.map((checkboxLanguage) => (
272
+ <ToLanguageCheckbox
273
+ key={checkboxLanguage.id}
274
+ {...{checkboxLanguage, fromLanguage, toLanguages, toggleToLanguage, languages}}
275
+ />
276
+ ))}
277
+ </Stack>
278
+ </Flex>
279
+ ) : (
280
+ <Flex padding={4} gap={2} align="flex-start" justify="center">
281
+ <Box>
282
+ <Spinner />
283
+ </Box>
284
+ <Text>Loading languages...</Text>
285
+ </Flex>
286
+ )}
287
+ </Dialog>
288
+ ) : null}
289
+ {props.children}
290
+ </FieldTranslationContext.Provider>
291
+ )
292
+ }
293
+
294
+ function ToLanguageCheckbox(props: {
295
+ checkboxLanguage: Language
296
+ fromLanguage: Language | undefined
297
+ toLanguages: Language[] | undefined
298
+ toggleToLanguage: (
299
+ toggledLang: Language,
300
+ toLanguages: Language[] | undefined,
301
+ languages: Language[] | undefined
302
+ ) => void
303
+ languages: Language[]
304
+ }) {
305
+ const {checkboxLanguage, fromLanguage, toLanguages, toggleToLanguage, languages} = props
306
+ const langId = checkboxLanguage.id
307
+ const onChange = useCallback(
308
+ () => toggleToLanguage(checkboxLanguage, toLanguages, languages),
309
+ [toggleToLanguage, checkboxLanguage, toLanguages, languages]
310
+ )
311
+ return (
312
+ <Flex
313
+ key={langId}
314
+ gap={3}
315
+ align="center"
316
+ as={'label'}
317
+ style={langId === fromLanguage?.id ? {opacity: 0.5} : undefined}
318
+ >
319
+ <Checkbox
320
+ name="toLang"
321
+ value={langId}
322
+ checked={langId !== fromLanguage?.id && !!toLanguages?.find((tl) => tl.id === langId)}
323
+ onChange={onChange}
324
+ disabled={langId === fromLanguage?.id}
325
+ />
326
+ <Text muted={langId === fromLanguage?.id}>{checkboxLanguage.title ?? langId}</Text>
327
+ </Flex>
328
+ )
329
+ }
330
+
331
+ function FromLanguageRadio(props: {
332
+ radioLanguage: Language
333
+ fromLanguage: Language | undefined
334
+ selectFromLanguage: (
335
+ from: Language,
336
+ languages: Language[] | undefined,
337
+ params: FieldTranslationParams | undefined
338
+ ) => void
339
+ languages: Language[] | undefined
340
+ fieldTranslationParams: FieldTranslationParams | undefined
341
+ }) {
342
+ const {languages, radioLanguage, selectFromLanguage, fromLanguage, fieldTranslationParams} = props
343
+ const langId = radioLanguage.id
344
+
345
+ const onChange = useCallback(
346
+ () => selectFromLanguage(radioLanguage, languages, fieldTranslationParams),
347
+ [selectFromLanguage, radioLanguage, languages, fieldTranslationParams]
348
+ )
349
+ return (
350
+ <Flex key={langId} gap={3} align="center" as={'label'}>
351
+ <Radio
352
+ name="fromLang"
353
+ value={langId}
354
+ checked={langId === fromLanguage?.id}
355
+ onChange={onChange}
356
+ />
357
+ <Text>{radioLanguage.title ?? radioLanguage.id}</Text>
358
+ </Flex>
359
+ )
360
+ }
@@ -0,0 +1,26 @@
1
+ import {SanityDocumentLike} from 'sanity'
2
+ import get from 'lodash/get'
3
+
4
+ export const getLanguageParams = (
5
+ select: Record<string, string> | undefined,
6
+ document: SanityDocumentLike | undefined
7
+ ): Record<string, unknown> => {
8
+ if (!select || !document) {
9
+ return {}
10
+ }
11
+
12
+ const selection: Record<string, string> = select || {}
13
+ const selectedValue: Record<string, unknown> = {}
14
+ for (const [key, path] of Object.entries(selection)) {
15
+ let value = get(document, path)
16
+ if (Array.isArray(value)) {
17
+ // If there are references in the array, ensure they have `_ref` set, otherwise they are considered empty and can safely be ignored
18
+ value = value.filter((item) =>
19
+ typeof item === 'object' ? item?._type !== 'reference' || '_ref' in item : true
20
+ )
21
+ }
22
+ selectedValue[key] = value
23
+ }
24
+
25
+ return selectedValue
26
+ }
@@ -0,0 +1,18 @@
1
+ export const toFieldLanguagesKeyPrefix = 'sanityStudio:assist:field-languages:from:'
2
+
3
+ export function getPreferredToFieldLanguages(fromLanguageId: string): string[] {
4
+ if (typeof localStorage === 'undefined') {
5
+ return []
6
+ }
7
+
8
+ const value = localStorage.getItem(`${toFieldLanguagesKeyPrefix}${fromLanguageId}`)
9
+ return value ? (JSON.parse(value) as string[]) : []
10
+ }
11
+
12
+ export function setPreferredToFieldLanguages(fromLanguageId: string, languageIds: string[]) {
13
+ if (typeof localStorage === 'undefined') {
14
+ return
15
+ }
16
+
17
+ localStorage.setItem(`${toFieldLanguagesKeyPrefix}${fromLanguageId}`, JSON.stringify(languageIds))
18
+ }