@sanity/assist 1.2.14 → 1.2.15-lang.2

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 (36) hide show
  1. package/README.md +392 -6
  2. package/dist/index.d.ts +170 -3
  3. package/dist/index.esm.js +2019 -125
  4. package/dist/index.esm.js.map +1 -1
  5. package/dist/index.js +2013 -119
  6. package/dist/index.js.map +1 -1
  7. package/package.json +12 -11
  8. package/src/_lib/form/DocumentForm.tsx +1 -1
  9. package/src/assistDocument/RequestRunInstructionProvider.tsx +37 -21
  10. package/src/assistDocument/components/instruction/InstructionInput.tsx +5 -4
  11. package/src/assistDocument/components/instruction/InstructionOutputField.tsx +45 -0
  12. package/src/assistDocument/components/instruction/InstructionOutputInput.tsx +205 -0
  13. package/src/assistDocument/hooks/useStudioAssistDocument.ts +5 -32
  14. package/src/assistFormComponents/AssistField.tsx +5 -4
  15. package/src/assistFormComponents/AssistFormBlock.tsx +2 -3
  16. package/src/assistFormComponents/validation/listItem.tsx +2 -2
  17. package/src/assistInspector/FieldAutocomplete.tsx +1 -0
  18. package/src/assistInspector/InstructionTaskHistoryButton.tsx +2 -3
  19. package/src/assistInspector/helpers.ts +7 -9
  20. package/src/assistLayout/AssistLayout.tsx +9 -6
  21. package/src/fieldActions/assistFieldActions.tsx +21 -8
  22. package/src/fieldActions/translateActions.tsx +141 -0
  23. package/src/helpers/assistSupported.ts +1 -1
  24. package/src/node_modules/.vitest/results.json +1 -0
  25. package/src/plugin.tsx +6 -0
  26. package/src/presence/AssistAvatar.tsx +1 -1
  27. package/src/schemas/assistDocumentSchema.tsx +39 -0
  28. package/src/schemas/serialize/serializeSchema.ts +6 -6
  29. package/src/schemas/typeDefExtensions.ts +12 -1
  30. package/src/translate/FieldTranslationProvider.tsx +267 -0
  31. package/src/translate/getLanguageParams.ts +26 -0
  32. package/src/translate/paths.test.ts +87 -0
  33. package/src/translate/paths.ts +151 -0
  34. package/src/translate/types.ts +159 -0
  35. package/src/types.ts +21 -2
  36. package/src/useApiClient.ts +63 -0
@@ -3,6 +3,7 @@ import {
3
3
  DocumentFieldActionGroup,
4
4
  DocumentFieldActionItem,
5
5
  ObjectSchemaType,
6
+ typed,
6
7
  useCurrentUser,
7
8
  } from 'sanity'
8
9
  import {ControlsIcon, SparklesIcon} from '@sanity/icons'
@@ -24,6 +25,7 @@ import {generateCaptionsActions} from './generateCaptionActions'
24
25
  import {useDocumentPane} from 'sanity/desk'
25
26
  import {useSelectedField, useTypePath} from '../assistInspector/helpers'
26
27
  import {isSchemaAssistEnabled} from '../helpers/assistSupported'
28
+ import {translateActions, TranslateProps} from './translateActions'
27
29
 
28
30
  function node(node: DocumentFieldActionItem | DocumentFieldActionGroup) {
29
31
  return node
@@ -47,6 +49,7 @@ export const assistFieldActions: DocumentFieldAction = {
47
49
  documentSchemaType,
48
50
  documentId,
49
51
  selectedPath,
52
+ assistableDocumentId,
50
53
  } =
51
54
  // document field actions do not have access to the document context
52
55
  // conditional hook _should_ be safe here since the logical path will be stable
@@ -89,7 +92,13 @@ export const assistFieldActions: DocumentFieldAction = {
89
92
  const isSelected = isInspectorOpen && isPathSelected
90
93
 
91
94
  const imageCaptionAction = generateCaptionsActions.useAction(props)
92
-
95
+ const translateAction = translateActions.useAction(
96
+ typed<TranslateProps>({
97
+ ...props,
98
+ documentId: assistableDocumentId,
99
+ documentIsAssistable,
100
+ })
101
+ )
93
102
  const manageInstructions = useCallback(
94
103
  () =>
95
104
  isSelected
@@ -134,7 +143,7 @@ export const assistFieldActions: DocumentFieldAction = {
134
143
  )
135
144
 
136
145
  const runInstructionsGroup = useMemo(() => {
137
- return instructions?.length || imageCaptionAction
146
+ return instructions?.length || imageCaptionAction || translateAction
138
147
  ? node({
139
148
  type: 'group',
140
149
  icon: () => null,
@@ -151,7 +160,7 @@ export const assistFieldActions: DocumentFieldAction = {
151
160
  })
152
161
  ),
153
162
  imageCaptionAction,
154
- ].filter(Boolean),
163
+ ].filter((a): a is DocumentFieldActionItem => !!a),
155
164
  expanded: true,
156
165
  })
157
166
  : undefined
@@ -163,6 +172,7 @@ export const assistFieldActions: DocumentFieldAction = {
163
172
  documentIsNew,
164
173
  assistSupported,
165
174
  imageCaptionAction,
175
+ translateAction,
166
176
  ])
167
177
 
168
178
  const instructionsLength = instructions?.length || 0
@@ -185,12 +195,14 @@ export const assistFieldActions: DocumentFieldAction = {
185
195
  type: 'group',
186
196
  icon: SparklesIcon,
187
197
  title: pluginTitleShort,
188
- children: [runInstructionsGroup, assistSupported && manageInstructionsItem].filter(
189
- (c): c is DocumentFieldActionItem | DocumentFieldActionGroup => !!c
190
- ),
198
+ children: [
199
+ runInstructionsGroup,
200
+ translateAction,
201
+ assistSupported && manageInstructionsItem,
202
+ ].filter((c): c is DocumentFieldActionItem | DocumentFieldActionGroup => !!c),
191
203
  expanded: false,
192
204
  renderAsButton: true,
193
- hidden: !assistSupported && !imageCaptionAction,
205
+ hidden: !assistSupported && !imageCaptionAction && !translateAction,
194
206
  }),
195
207
  [
196
208
  //documentIsNew,
@@ -198,6 +210,7 @@ export const assistFieldActions: DocumentFieldAction = {
198
210
  manageInstructionsItem,
199
211
  assistSupported,
200
212
  imageCaptionAction,
213
+ translateAction,
201
214
  ]
202
215
  )
203
216
 
@@ -216,7 +229,7 @@ export const assistFieldActions: DocumentFieldAction = {
216
229
  )
217
230
 
218
231
  // If there are no instructions, we don't want to render the group
219
- if (instructionsLength === 0 && !imageCaptionAction) {
232
+ if (instructionsLength === 0 && !imageCaptionAction && !translateAction) {
220
233
  return emptyAction
221
234
  }
222
235
 
@@ -0,0 +1,141 @@
1
+ /* eslint-disable react-hooks/rules-of-hooks */
2
+ import {
3
+ DocumentFieldAction,
4
+ DocumentFieldActionGroup,
5
+ DocumentFieldActionItem,
6
+ DocumentFieldActionProps,
7
+ ObjectSchemaType,
8
+ } from 'sanity'
9
+ import {TranslateIcon} from '@sanity/icons'
10
+ import {useMemo, useRef} from 'react'
11
+ import {useApiClient, useTranslate} from '../useApiClient'
12
+ import {useAiAssistanceConfig} from '../assistLayout/AiAssistanceConfigContext'
13
+ import {Box, Spinner} from '@sanity/ui'
14
+ import {isAssistSupported} from '../helpers/assistSupported'
15
+ import {useDocumentPane} from 'sanity/desk'
16
+ import {useFieldTranslation} from '../translate/FieldTranslationProvider'
17
+ import {useDraftDelayedTask} from '../assistDocument/RequestRunInstructionProvider'
18
+ import {getLanguageParams} from '../translate/getLanguageParams'
19
+
20
+ function node(node: DocumentFieldActionItem | DocumentFieldActionGroup) {
21
+ return node
22
+ }
23
+
24
+ export type TranslateProps = DocumentFieldActionProps & {documentIsAssistable?: boolean}
25
+ export const translateActions: DocumentFieldAction = {
26
+ name: 'sanity-assist-translate',
27
+ useAction(props: TranslateProps) {
28
+ const {config} = useAiAssistanceConfig()
29
+ const apiClient = useApiClient(config?.__customApiClient)
30
+
31
+ const isDocumentLevel = props.path.length === 0
32
+ const {schemaType, documentId, documentIsAssistable} = props
33
+
34
+ const fieldTransEnabled = config.translate?.field?.documentTypes?.includes(schemaType.name)
35
+ const docTransTypes = config.translate?.document?.documentTypes
36
+ const documentTranslationEnabled =
37
+ isDocumentLevel &&
38
+ ((!docTransTypes && isAssistSupported(schemaType)) ||
39
+ docTransTypes?.includes(schemaType.name))
40
+
41
+ // these checks are stable (ie, does not change after mount), so not breaking rules of hooks
42
+ if (documentTranslationEnabled || fieldTransEnabled) {
43
+ const {value: documentValue, onChange: documentOnChange} = useDocumentPane()
44
+ const docRef = useRef(documentValue)
45
+
46
+ const translationApi = useTranslate(apiClient)
47
+ const translate = useDraftDelayedTask({
48
+ documentOnChange,
49
+ isDocAssistable: documentIsAssistable ?? false,
50
+ task: translationApi.translate,
51
+ })
52
+ docRef.current = documentValue
53
+ const languagePath = config.translate?.document?.languageField
54
+
55
+ //const {value: languageId} = extractWithPath(languagePath, documentValue)[0] ?? {}
56
+ // if this is true, it is stable, and not breaking rules of hooks
57
+ const translateDocumentAction = useMemo(
58
+ () =>
59
+ languagePath && documentTranslationEnabled
60
+ ? node({
61
+ type: 'action',
62
+ icon: translationApi.loading
63
+ ? () => (
64
+ <Box style={{height: 17}}>
65
+ <Spinner style={{transform: 'translateY(6px)'}} />
66
+ </Box>
67
+ )
68
+ : TranslateIcon,
69
+ title: `Translate document`,
70
+ onAction: () => {
71
+ if (translationApi.loading || !languagePath || !documentId) {
72
+ return
73
+ }
74
+ translate({languagePath, documentId: documentId ?? ''})
75
+ },
76
+ renderAsButton: true,
77
+ disabled: translationApi.loading,
78
+ })
79
+ : undefined,
80
+ [languagePath, translate, documentId, translationApi.loading, documentTranslationEnabled]
81
+ )
82
+
83
+ const fieldTranslate = useFieldTranslation()
84
+ const openFieldTranslation = useDraftDelayedTask({
85
+ documentOnChange,
86
+ isDocAssistable: documentIsAssistable ?? false,
87
+ task: fieldTranslate.openFieldTranslation,
88
+ })
89
+
90
+ const translateFieldsAction = useMemo(
91
+ () =>
92
+ fieldTransEnabled
93
+ ? node({
94
+ type: 'action',
95
+ icon: fieldTranslate.translationLoading
96
+ ? () => (
97
+ <Box style={{height: 17}}>
98
+ <Spinner style={{transform: 'translateY(6px)'}} />
99
+ </Box>
100
+ )
101
+ : TranslateIcon,
102
+ title: `Translate fields`,
103
+ onAction: () => {
104
+ if (fieldTranslate.translationLoading || !documentId) {
105
+ return
106
+ }
107
+ openFieldTranslation({
108
+ document: docRef.current,
109
+ documentSchema: schemaType as ObjectSchemaType,
110
+ })
111
+ },
112
+ renderAsButton: true,
113
+ disabled: fieldTranslate.translationLoading,
114
+ })
115
+ : undefined,
116
+ [
117
+ openFieldTranslation,
118
+ schemaType,
119
+ documentId,
120
+ fieldTranslate.translationLoading,
121
+ fieldTransEnabled,
122
+ ]
123
+ )
124
+
125
+ // eslint-disable-next-line react-hooks/rules-of-hooks
126
+ return useMemo(() => {
127
+ return node({
128
+ type: 'group',
129
+ icon: () => null,
130
+ title: 'Translate',
131
+ children: [translateDocumentAction, translateFieldsAction].filter(
132
+ (c): c is DocumentFieldActionItem => !!c
133
+ ),
134
+ expanded: true,
135
+ })
136
+ }, [translateDocumentAction, translateFieldsAction])
137
+ }
138
+ // works but not supported by types
139
+ return undefined as unknown as DocumentFieldActionItem
140
+ },
141
+ }
@@ -43,7 +43,7 @@ function isUnsupportedType(type: SchemaType) {
43
43
  type.jsonType === 'number' ||
44
44
  type.name === 'sanity.imageCrop' ||
45
45
  type.name === 'sanity.imageHotspot' ||
46
- isType(type, 'reference') ||
46
+ (isType(type, 'reference') && !type?.options?.aiWritingAssistance?.embeddingsIndex) ||
47
47
  isType(type, 'crossDatasetReference') ||
48
48
  isType(type, 'slug') ||
49
49
  isType(type, 'url') ||
@@ -0,0 +1 @@
1
+ {"version":"0.32.2","results":[[":schemas/serialize/serializeSchema.test.ts",{"duration":12,"failed":false}],[":translate/paths.test.ts",{"duration":6,"failed":false}]]}
package/src/plugin.tsx CHANGED
@@ -15,8 +15,11 @@ import {createAssistDocumentPresence} from './presence/AssistDocumentPresence'
15
15
  import {isSchemaAssistEnabled} from './helpers/assistSupported'
16
16
  import {isImage} from './helpers/typeUtils'
17
17
  import {ImageContextProvider} from './components/ImageContext'
18
+ import {TranslationConfig} from './translate/types'
18
19
 
19
20
  export interface AssistPluginConfig {
21
+ translate?: TranslationConfig
22
+
20
23
  /**
21
24
  * Set this to false to disable model migration from the alpha version of this plugin
22
25
  */
@@ -36,6 +39,9 @@ export const assist = definePlugin<AssistPluginConfig | void>((config) => {
36
39
  schema: {
37
40
  types: schemaTypes,
38
41
  },
42
+ i18n: {
43
+ bundles: [{}],
44
+ },
39
45
 
40
46
  document: {
41
47
  inspectors: (prev, context) => {
@@ -88,7 +88,7 @@ export function AssistAvatar(props: {state?: 'present' | 'active'}) {
88
88
  </Outline>
89
89
  <IconDisc>
90
90
  <Text as="span" size={0} style={{color: 'inherit'}}>
91
- <SparklesIcon />
91
+ <SparklesIcon style={{color: 'inherit'}} />
92
92
  </Text>
93
93
  </IconDisc>
94
94
  </Root>
@@ -18,6 +18,8 @@ import {
18
18
  instructionContextTypeName,
19
19
  instructionTaskTypeName,
20
20
  instructionTypeName,
21
+ outputFieldTypeName,
22
+ outputTypeTypeName,
21
23
  promptTypeName,
22
24
  userInputTypeName,
23
25
  } from '../types'
@@ -37,6 +39,8 @@ import {PromptInput} from '../assistDocument/components/instruction/PromptInput'
37
39
  import {instructionGuideUrl} from '../constants'
38
40
  import {InstructionsArrayField} from '../assistDocument/components/InstructionsArrayField'
39
41
  import {getFieldRefsWithDocument} from '../assistInspector/helpers'
42
+ import {InstructionOutputField} from '../assistDocument/components/instruction/InstructionOutputField'
43
+ import {InstructionOutputInput} from '../assistDocument/components/instruction/InstructionOutputInput'
40
44
 
41
45
  export const fieldReference = defineType({
42
46
  type: 'object',
@@ -329,6 +333,41 @@ export const instruction = defineType({
329
333
  return context.currentUser?.id ?? ''
330
334
  },
331
335
  }),
336
+ defineField({
337
+ type: 'array',
338
+ name: 'output',
339
+ title: 'Output filter',
340
+ components: {
341
+ input: InstructionOutputInput,
342
+ field: InstructionOutputField,
343
+ },
344
+ of: [
345
+ defineArrayMember({
346
+ type: 'object' as const,
347
+ name: outputFieldTypeName,
348
+ title: 'Output field',
349
+ fields: [
350
+ {
351
+ type: 'string',
352
+ name: 'path',
353
+ title: 'Path',
354
+ },
355
+ ],
356
+ }),
357
+ defineArrayMember({
358
+ type: 'object' as const,
359
+ name: outputTypeTypeName,
360
+ title: 'Output type',
361
+ fields: [
362
+ {
363
+ type: 'string',
364
+ name: 'type',
365
+ title: 'Type',
366
+ },
367
+ ],
368
+ }),
369
+ ],
370
+ }),
332
371
  ],
333
372
  })
334
373
 
@@ -2,6 +2,7 @@ import {
2
2
  ArraySchemaType,
3
3
  ImageOptions,
4
4
  ObjectSchemaType,
5
+ ReferenceOptions,
5
6
  ReferenceSchemaType,
6
7
  Schema,
7
8
  SchemaType,
@@ -78,13 +79,12 @@ function getBaseFields(
78
79
  typeName: string,
79
80
  options: Options | undefined
80
81
  ) {
81
- const imagePromptField = (type.options as ImageOptions)?.imagePromptField
82
+ const schemaOptions = removeUndef({
83
+ imagePromptField: (type.options as ImageOptions)?.imagePromptField,
84
+ embeddingsIndex: (type.options as ReferenceOptions)?.aiWritingAssistance?.embeddingsIndex,
85
+ })
82
86
  return removeUndef({
83
- options: imagePromptField
84
- ? {
85
- imagePromptField: imagePromptField,
86
- }
87
- : undefined,
87
+ options: Object.keys(schemaOptions).length ? schemaOptions : undefined,
88
88
  values: Array.isArray(type?.options?.list)
89
89
  ? type?.options?.list.map((v: string | {value: string; title: string}) =>
90
90
  typeof v === 'string' ? v : v.value ?? `${v.title}`
@@ -22,7 +22,18 @@ declare module 'sanity' {
22
22
  }
23
23
  interface NumberOptions extends AssistOptions {}
24
24
  interface ObjectOptions extends AssistOptions {}
25
- interface ReferenceBaseOptions extends AssistOptions {}
25
+ interface ReferenceBaseOptions {
26
+ aiWritingAssistance?: {
27
+ /** Set to true to disable assistance for this field or type */
28
+ exclude?: boolean
29
+
30
+ /**
31
+ * When set, the reference field will allow instructions to be added to it.
32
+ * Should be the name of the embeddings-index where assist will look for contextually relevant documents
33
+ * */
34
+ embeddingsIndex?: string
35
+ }
36
+ }
26
37
  interface SlugOptions extends AssistOptions {}
27
38
  interface StringOptions extends AssistOptions {}
28
39
  interface TextOptions extends AssistOptions {}
@@ -0,0 +1,267 @@
1
+ import {
2
+ createContext,
3
+ PropsWithChildren,
4
+ useCallback,
5
+ useContext,
6
+ useId,
7
+ useMemo,
8
+ useState,
9
+ } from 'react'
10
+ import {ObjectSchemaType, 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
+ getDocumentMembersFlat,
17
+ getTranslationMap,
18
+ TranslationMap,
19
+ } from './paths'
20
+ import {PlayIcon} from '@sanity/icons'
21
+ import {Language} from './types'
22
+ import {getLanguageParams} from './getLanguageParams'
23
+
24
+ export interface FieldTranslationContextValue {
25
+ openFieldTranslation: (args: {
26
+ document: SanityDocumentLike
27
+ documentSchema: ObjectSchemaType
28
+ }) => void
29
+ translationLoading: boolean
30
+ //loadLanguages: (document: SanityDocumentLike, documentSchema: ObjectSchemaType) => void
31
+ }
32
+
33
+ export const FieldTranslationContext = createContext<FieldTranslationContextValue>({
34
+ openFieldTranslation: () => {},
35
+ translationLoading: false,
36
+ //loadLanguages: () => {},
37
+ })
38
+
39
+ export function useFieldTranslation() {
40
+ return useContext(FieldTranslationContext)
41
+ }
42
+
43
+ export function FieldTranslationProvider(props: PropsWithChildren<{}>) {
44
+ const {config: assistConfig} = useAiAssistanceConfig()
45
+ const apiClient = useApiClient(assistConfig.__customApiClient)
46
+ const config = assistConfig.translate?.field
47
+ const {translate: runTranslate} = useTranslate(apiClient)
48
+
49
+ const [dialogOpen, setDialogOpen] = useState(false)
50
+
51
+ const [document, setDocument] = useState<SanityDocumentLike | undefined>()
52
+ const [documentSchema, setDocumentSchema] = useState<ObjectSchemaType | undefined>()
53
+ const [languages, setLanguages] = useState<Language[] | undefined>()
54
+ const [fromLanguage, setFromLanguage] = useState<Language | undefined>(undefined)
55
+ const [toLanguages, setToLanguages] = useState<Language[] | undefined>(undefined)
56
+ const [translationMap, setTranslationMap] = useState<TranslationMap[] | undefined>()
57
+
58
+ const close = useCallback(() => {
59
+ setDialogOpen(false)
60
+ setLanguages(undefined)
61
+ setDocument(undefined)
62
+ setDocument(undefined)
63
+ }, [])
64
+ const languageClient = useClient({apiVersion: config?.apiVersion ?? '2022-11-27'})
65
+ const documentId = document?._id
66
+ const id = useId()
67
+
68
+ const selectFromLanguage = useCallback(
69
+ (
70
+ from: Language,
71
+ languages: Language[] | undefined,
72
+ document: SanityDocumentLike | undefined,
73
+ documentSchema: ObjectSchemaType | undefined
74
+ ) => {
75
+ setFromLanguage(from)
76
+ if (!document || !documentSchema || !languages) {
77
+ setTranslationMap(undefined)
78
+ return
79
+ }
80
+
81
+ const to = languages.filter((l) => l.id !== from?.id)
82
+ setToLanguages(to)
83
+ const fromId = from?.id
84
+ const toIds = to?.map((l) => l.id) ?? []
85
+ const docMembers = getDocumentMembersFlat(document, documentSchema)
86
+ if (fromId && toIds?.length) {
87
+ const transMap = getTranslationMap(
88
+ documentSchema,
89
+ docMembers,
90
+ fromId,
91
+ toIds,
92
+ config?.translationOutputs ?? defaultLanguageOutputs
93
+ )
94
+ setTranslationMap(transMap)
95
+ } else {
96
+ setTranslationMap(undefined)
97
+ }
98
+ },
99
+ [config]
100
+ )
101
+
102
+ const toggleToLanguage = useCallback(
103
+ (
104
+ toggledLang: Language,
105
+ toLanguages: Language[] | undefined,
106
+ languages: Language[] | undefined
107
+ ) => {
108
+ if (!languages) {
109
+ return
110
+ }
111
+ const wasSelected = !!toLanguages?.find((l) => l.id === toggledLang.id)
112
+ const newToLangs = languages.filter(
113
+ (anyLang) =>
114
+ !!toLanguages?.find(
115
+ (selectedLang) => toggledLang.id !== selectedLang.id && selectedLang.id === anyLang.id
116
+ ) ||
117
+ (toggledLang.id === anyLang.id && !wasSelected)
118
+ )
119
+ setToLanguages(newToLangs)
120
+ },
121
+ []
122
+ )
123
+
124
+ const openFieldTranslation = useCallback(
125
+ async ({
126
+ document,
127
+ documentSchema,
128
+ }: {
129
+ document: SanityDocumentLike
130
+ documentSchema: ObjectSchemaType
131
+ }) => {
132
+ setDialogOpen(true)
133
+ const languageParams = getLanguageParams(config?.selectLanguageParams, document)
134
+ const languages: Language[] | undefined = await (typeof config?.languages === 'function'
135
+ ? config?.languages(languageClient, languageParams)
136
+ : Promise.resolve(config?.languages))
137
+ setLanguages(languages)
138
+ setDocument(document)
139
+ setDocumentSchema(documentSchema)
140
+ const fromLanguage = languages?.[0]
141
+ if (fromLanguage) {
142
+ selectFromLanguage(fromLanguage, languages, document, documentSchema)
143
+ } else {
144
+ console.error('No languages available for selected language params', languageParams)
145
+ }
146
+ },
147
+ [selectFromLanguage, config, languageClient]
148
+ )
149
+
150
+ const contextValue: FieldTranslationContextValue = useMemo(() => {
151
+ return {
152
+ openFieldTranslation,
153
+ translationLoading: false,
154
+ }
155
+ }, [openFieldTranslation])
156
+
157
+ const runDisabled =
158
+ !fromLanguage || !toLanguages?.length || !translationMap?.length || !documentId
159
+
160
+ const onRunTranslation = useCallback(() => {
161
+ if (translationMap && documentId) {
162
+ runTranslate({
163
+ documentId,
164
+ fieldLanguageMap: translationMap.map((map) => ({
165
+ ...map,
166
+ // eslint-disable-next-line max-nested-callbacks
167
+ outputs: map.outputs.filter((out) => !!toLanguages?.find((l) => l.id === out.id)),
168
+ })),
169
+ })
170
+ }
171
+ close()
172
+ }, [translationMap, documentId, runTranslate, close, toLanguages])
173
+
174
+ const runButton = (
175
+ <Button
176
+ text={`Translate`}
177
+ tone="primary"
178
+ icon={PlayIcon}
179
+ style={{width: '100%'}}
180
+ disabled={runDisabled}
181
+ onClick={onRunTranslation}
182
+ />
183
+ )
184
+
185
+ return (
186
+ <FieldTranslationContext.Provider value={contextValue}>
187
+ {dialogOpen ? (
188
+ <Dialog
189
+ id={id}
190
+ width={1}
191
+ open={dialogOpen}
192
+ onClose={close}
193
+ header="Translate fields"
194
+ footer={
195
+ <Flex justify="space-between" padding={2} flex={1}>
196
+ {runDisabled ? (
197
+ <Tooltip
198
+ content={
199
+ <Flex padding={2}>
200
+ <Text>Nothing to translate.</Text>
201
+ </Flex>
202
+ }
203
+ placement="top"
204
+ >
205
+ <Flex flex={1}>{runButton}</Flex>
206
+ </Tooltip>
207
+ ) : (
208
+ runButton
209
+ )}
210
+ </Flex>
211
+ }
212
+ >
213
+ {languages ? (
214
+ <Flex padding={4} gap={5} align="flex-start" justify="center">
215
+ <Stack space={2}>
216
+ <Box marginBottom={2}>
217
+ <Text weight="semibold">From</Text>
218
+ </Box>
219
+ {languages?.map((l) => (
220
+ <Flex key={l.id} gap={3} align="center">
221
+ <Radio
222
+ name="fromLang"
223
+ value={l.id}
224
+ checked={l.id === fromLanguage?.id}
225
+ onClick={() => selectFromLanguage(l, languages, document, documentSchema)}
226
+ />
227
+ <Text>{l.title ?? l.id}</Text>
228
+ </Flex>
229
+ ))}
230
+ </Stack>
231
+
232
+ <Stack space={2}>
233
+ <Box marginBottom={2}>
234
+ <Text weight="semibold">To</Text>
235
+ </Box>
236
+ {languages
237
+ ?.filter((l) => l.id !== fromLanguage?.id)
238
+ .map((l) => (
239
+ <Flex key={l.id} gap={3} align="center">
240
+ <Checkbox
241
+ name="toLang"
242
+ value={l.id}
243
+ checked={!!toLanguages?.find((tl) => tl.id === l.id)}
244
+ onClick={() => toggleToLanguage(l, toLanguages, languages)}
245
+ disabled={
246
+ !translationMap?.find((tm) => tm.outputs.find((o) => o.id === l.id))
247
+ }
248
+ />
249
+ <Text>{l.title ?? l.id}</Text>
250
+ </Flex>
251
+ ))}
252
+ </Stack>
253
+ </Flex>
254
+ ) : (
255
+ <Flex padding={4} gap={2} align="flex-start" justify="center">
256
+ <Box>
257
+ <Spinner />
258
+ </Box>
259
+ <Text>Loading languages...</Text>
260
+ </Flex>
261
+ )}
262
+ </Dialog>
263
+ ) : null}
264
+ {props.children}
265
+ </FieldTranslationContext.Provider>
266
+ )
267
+ }
@@ -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
+ }