@sanity/assist 1.2.13 → 1.2.15-lang.1
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/README.md +392 -6
- package/dist/index.d.ts +170 -3
- package/dist/index.esm.js +1986 -111
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1980 -105
- package/dist/index.js.map +1 -1
- package/package.json +15 -14
- package/src/_lib/form/DocumentForm.tsx +1 -1
- package/src/assistDocument/components/instruction/InstructionInput.tsx +5 -4
- package/src/assistDocument/components/instruction/InstructionOutputField.tsx +45 -0
- package/src/assistDocument/components/instruction/InstructionOutputInput.tsx +205 -0
- package/src/assistDocument/hooks/useStudioAssistDocument.ts +5 -32
- package/src/assistFormComponents/AssistField.tsx +5 -4
- package/src/assistFormComponents/AssistFormBlock.tsx +2 -3
- package/src/assistFormComponents/validation/listItem.tsx +2 -2
- package/src/assistInspector/FieldAutocomplete.tsx +1 -0
- package/src/assistInspector/InstructionTaskHistoryButton.tsx +2 -3
- package/src/assistInspector/helpers.ts +7 -9
- package/src/assistLayout/AssistLayout.tsx +9 -6
- package/src/fieldActions/assistFieldActions.tsx +14 -8
- package/src/fieldActions/translateActions.tsx +118 -0
- package/src/helpers/assistSupported.ts +1 -1
- package/src/node_modules/.vitest/results.json +1 -0
- package/src/plugin.tsx +12 -2
- package/src/presence/AssistAvatar.tsx +1 -1
- package/src/schemas/assistDocumentSchema.tsx +39 -0
- package/src/schemas/serialize/serializeSchema.test.ts +15 -2
- package/src/schemas/serialize/serializeSchema.ts +8 -7
- package/src/schemas/typeDefExtensions.ts +12 -1
- package/src/translate/FieldTranslationProvider.tsx +254 -0
- package/src/translate/getLanguageParams.ts +26 -0
- package/src/translate/paths.test.ts +87 -0
- package/src/translate/paths.ts +151 -0
- package/src/translate/types.ts +159 -0
- package/src/types.ts +21 -2
- package/src/useApiClient.ts +63 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/* eslint-disable react-hooks/rules-of-hooks */
|
|
2
|
+
import {
|
|
3
|
+
DocumentFieldAction,
|
|
4
|
+
DocumentFieldActionGroup,
|
|
5
|
+
DocumentFieldActionItem,
|
|
6
|
+
ObjectSchemaType,
|
|
7
|
+
} from 'sanity'
|
|
8
|
+
import {TranslateIcon} from '@sanity/icons'
|
|
9
|
+
import {useMemo, useRef} from 'react'
|
|
10
|
+
import {useApiClient, useTranslate} from '../useApiClient'
|
|
11
|
+
import {useAiAssistanceConfig} from '../assistLayout/AiAssistanceConfigContext'
|
|
12
|
+
import {Box, Spinner} from '@sanity/ui'
|
|
13
|
+
import {isAssistSupported} from '../helpers/assistSupported'
|
|
14
|
+
import {useDocumentPane} from 'sanity/desk'
|
|
15
|
+
import {useFieldTranslation} from '../translate/FieldTranslationProvider'
|
|
16
|
+
|
|
17
|
+
function node(node: DocumentFieldActionItem | DocumentFieldActionGroup) {
|
|
18
|
+
return node
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const translateActions: DocumentFieldAction = {
|
|
22
|
+
name: 'sanity-assist-translate',
|
|
23
|
+
useAction(props) {
|
|
24
|
+
const {config} = useAiAssistanceConfig()
|
|
25
|
+
const apiClient = useApiClient(config?.__customApiClient)
|
|
26
|
+
const {translate, loading} = useTranslate(apiClient)
|
|
27
|
+
|
|
28
|
+
const isDocumentLevel = props.path.length === 0
|
|
29
|
+
const {schemaType, documentId} = props
|
|
30
|
+
|
|
31
|
+
if (isDocumentLevel) {
|
|
32
|
+
const {value: documentValue} = useDocumentPane()
|
|
33
|
+
const docRef = useRef(documentValue)
|
|
34
|
+
docRef.current = documentValue
|
|
35
|
+
|
|
36
|
+
const docTransTypes = config.translate?.document?.documentTypes
|
|
37
|
+
const documentTranslation =
|
|
38
|
+
(!docTransTypes && isAssistSupported(schemaType)) ||
|
|
39
|
+
docTransTypes?.includes(schemaType.name)
|
|
40
|
+
const languagePath = config.translate?.document?.languageField
|
|
41
|
+
|
|
42
|
+
//const {value: languageId} = extractWithPath(languagePath, documentValue)[0] ?? {}
|
|
43
|
+
// if this is true, it is stable, and not breaking rules of hooks
|
|
44
|
+
const translateDocumentAction = useMemo(
|
|
45
|
+
() =>
|
|
46
|
+
languagePath && documentTranslation
|
|
47
|
+
? node({
|
|
48
|
+
type: 'action',
|
|
49
|
+
icon: loading
|
|
50
|
+
? () => (
|
|
51
|
+
<Box style={{height: 17}}>
|
|
52
|
+
<Spinner style={{transform: 'translateY(6px)'}} />
|
|
53
|
+
</Box>
|
|
54
|
+
)
|
|
55
|
+
: TranslateIcon,
|
|
56
|
+
title: `Translate document`,
|
|
57
|
+
onAction: () => {
|
|
58
|
+
if (loading || !languagePath || !documentId) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
translate({languagePath, documentId: documentId ?? ''})
|
|
62
|
+
},
|
|
63
|
+
renderAsButton: true,
|
|
64
|
+
disabled: loading,
|
|
65
|
+
})
|
|
66
|
+
: undefined,
|
|
67
|
+
[languagePath, translate, documentId, loading, documentTranslation]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const fieldTranslate = useFieldTranslation()
|
|
71
|
+
|
|
72
|
+
const fieldTransEnabled = config.translate?.field?.documentTypes?.includes(schemaType.name)
|
|
73
|
+
const translateFieldsAction = useMemo(
|
|
74
|
+
() =>
|
|
75
|
+
fieldTransEnabled
|
|
76
|
+
? node({
|
|
77
|
+
type: 'action',
|
|
78
|
+
icon: loading
|
|
79
|
+
? () => (
|
|
80
|
+
<Box style={{height: 17}}>
|
|
81
|
+
<Spinner style={{transform: 'translateY(6px)'}} />
|
|
82
|
+
</Box>
|
|
83
|
+
)
|
|
84
|
+
: TranslateIcon,
|
|
85
|
+
title: `Translate fields`,
|
|
86
|
+
onAction: () => {
|
|
87
|
+
if (loading || !documentId) {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
fieldTranslate.openFieldTranslation(
|
|
91
|
+
docRef.current,
|
|
92
|
+
schemaType as ObjectSchemaType
|
|
93
|
+
)
|
|
94
|
+
},
|
|
95
|
+
renderAsButton: true,
|
|
96
|
+
disabled: loading,
|
|
97
|
+
})
|
|
98
|
+
: undefined,
|
|
99
|
+
[fieldTranslate, schemaType, documentId, loading, fieldTransEnabled]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
103
|
+
return useMemo(() => {
|
|
104
|
+
return node({
|
|
105
|
+
type: 'group',
|
|
106
|
+
icon: () => null,
|
|
107
|
+
title: 'Translate',
|
|
108
|
+
children: [translateDocumentAction, translateFieldsAction].filter(
|
|
109
|
+
(c): c is DocumentFieldActionItem => !!c
|
|
110
|
+
),
|
|
111
|
+
expanded: true,
|
|
112
|
+
})
|
|
113
|
+
}, [translateDocumentAction, translateFieldsAction])
|
|
114
|
+
}
|
|
115
|
+
// works but not supported by types
|
|
116
|
+
return undefined as unknown as DocumentFieldActionItem
|
|
117
|
+
},
|
|
118
|
+
}
|
|
@@ -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) => {
|
|
@@ -45,8 +51,12 @@ export const assist = definePlugin<AssistPluginConfig | void>((config) => {
|
|
|
45
51
|
}
|
|
46
52
|
return prev
|
|
47
53
|
},
|
|
48
|
-
unstable_fieldActions: (prev) => {
|
|
49
|
-
|
|
54
|
+
unstable_fieldActions: (prev, {documentType, schema}) => {
|
|
55
|
+
const docSchema = schema.get(documentType)
|
|
56
|
+
if (docSchema && isSchemaAssistEnabled(docSchema)) {
|
|
57
|
+
return [...prev, assistFieldActions]
|
|
58
|
+
}
|
|
59
|
+
return prev
|
|
50
60
|
},
|
|
51
61
|
unstable_languageFilter: (prev, {documentId, schema, schemaType}) => {
|
|
52
62
|
const docSchema = schema.get(schemaType)
|
|
@@ -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
|
|
|
@@ -23,7 +23,7 @@ const mockStudioTypes = [
|
|
|
23
23
|
]
|
|
24
24
|
|
|
25
25
|
describe('serializeSchema', () => {
|
|
26
|
-
test('should
|
|
26
|
+
test('should serialize excluded document schema to support exclude: false overrides at the field level', () => {
|
|
27
27
|
const schema = Schema.compile({
|
|
28
28
|
name: 'test',
|
|
29
29
|
types: [
|
|
@@ -42,7 +42,20 @@ describe('serializeSchema', () => {
|
|
|
42
42
|
|
|
43
43
|
const serializedTypes = serializeSchema(schema, {leanFormat: true})
|
|
44
44
|
|
|
45
|
-
expect(serializedTypes).toEqual([
|
|
45
|
+
expect(serializedTypes).toEqual([
|
|
46
|
+
{
|
|
47
|
+
fields: [
|
|
48
|
+
{
|
|
49
|
+
name: 'title',
|
|
50
|
+
title: 'Title',
|
|
51
|
+
type: 'string',
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
name: 'article',
|
|
55
|
+
title: 'Article',
|
|
56
|
+
type: 'document',
|
|
57
|
+
},
|
|
58
|
+
])
|
|
46
59
|
})
|
|
47
60
|
|
|
48
61
|
test('should serialize simple schema', () => {
|
|
@@ -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,
|
|
@@ -29,7 +30,8 @@ export function serializeSchema(schema: Schema, options?: Options): SerializedSc
|
|
|
29
30
|
.filter((t) => !(hiddenTypes.includes(t) || t.startsWith('sanity.')))
|
|
30
31
|
.map((t) => schema.get(t))
|
|
31
32
|
.filter((t): t is SchemaType => !!t)
|
|
32
|
-
|
|
33
|
+
// because a field can override exclude at the type level, we have to also serialize excluded types
|
|
34
|
+
// so don't do this: .filter((t) => isAssistSupported(t))
|
|
33
35
|
.filter((t) => !t.hidden && !t.readOnly)
|
|
34
36
|
.map((t) => getSchemaStub(t, schema, options))
|
|
35
37
|
.filter((t) => {
|
|
@@ -77,13 +79,12 @@ function getBaseFields(
|
|
|
77
79
|
typeName: string,
|
|
78
80
|
options: Options | undefined
|
|
79
81
|
) {
|
|
80
|
-
const
|
|
82
|
+
const schemaOptions = removeUndef({
|
|
83
|
+
imagePromptField: (type.options as ImageOptions)?.imagePromptField,
|
|
84
|
+
embeddingsIndex: (type.options as ReferenceOptions)?.aiWritingAssistance?.embeddingsIndex,
|
|
85
|
+
})
|
|
81
86
|
return removeUndef({
|
|
82
|
-
options:
|
|
83
|
-
? {
|
|
84
|
-
imagePromptField: imagePromptField,
|
|
85
|
-
}
|
|
86
|
-
: undefined,
|
|
87
|
+
options: Object.keys(schemaOptions).length ? schemaOptions : undefined,
|
|
87
88
|
values: Array.isArray(type?.options?.list)
|
|
88
89
|
? type?.options?.list.map((v: string | {value: string; title: string}) =>
|
|
89
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
|
|
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,254 @@
|
|
|
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: (document: SanityDocumentLike, documentSchema: ObjectSchemaType) => void
|
|
26
|
+
translationLoading: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const FieldTranslationContext = createContext<FieldTranslationContextValue>({
|
|
30
|
+
openFieldTranslation: () => {},
|
|
31
|
+
translationLoading: false,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export function useFieldTranslation() {
|
|
35
|
+
return useContext(FieldTranslationContext)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function FieldTranslationProvider(props: PropsWithChildren<{}>) {
|
|
39
|
+
const {config: assistConfig} = useAiAssistanceConfig()
|
|
40
|
+
const apiClient = useApiClient(assistConfig.__customApiClient)
|
|
41
|
+
const config = assistConfig.translate?.field
|
|
42
|
+
const {translate: runTranslate} = useTranslate(apiClient)
|
|
43
|
+
|
|
44
|
+
const [dialogOpen, setDialogOpen] = useState(false)
|
|
45
|
+
|
|
46
|
+
const [document, setDocument] = useState<SanityDocumentLike | undefined>()
|
|
47
|
+
const [documentSchema, setDocumentSchema] = useState<ObjectSchemaType | undefined>()
|
|
48
|
+
const [languages, setLanguages] = useState<Language[] | undefined>()
|
|
49
|
+
const [fromLanguage, setFromLanguage] = useState<Language | undefined>(undefined)
|
|
50
|
+
const [toLanguages, setToLanguages] = useState<Language[] | undefined>(undefined)
|
|
51
|
+
const [translationMap, setTranslationMap] = useState<TranslationMap[] | undefined>()
|
|
52
|
+
|
|
53
|
+
const close = useCallback(() => {
|
|
54
|
+
setDialogOpen(false)
|
|
55
|
+
setLanguages(undefined)
|
|
56
|
+
setDocument(undefined)
|
|
57
|
+
setDocument(undefined)
|
|
58
|
+
}, [])
|
|
59
|
+
const languageClient = useClient({apiVersion: config?.apiVersion ?? '2022-11-27'})
|
|
60
|
+
const documentId = document?._id
|
|
61
|
+
const id = useId()
|
|
62
|
+
|
|
63
|
+
const selectFromLanguage = useCallback(
|
|
64
|
+
(
|
|
65
|
+
from: Language,
|
|
66
|
+
languages: Language[] | undefined,
|
|
67
|
+
document: SanityDocumentLike | undefined,
|
|
68
|
+
documentSchema: ObjectSchemaType | undefined
|
|
69
|
+
) => {
|
|
70
|
+
setFromLanguage(from)
|
|
71
|
+
if (!document || !documentSchema || !languages) {
|
|
72
|
+
setTranslationMap(undefined)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const to = languages.filter((l) => l.id !== from?.id)
|
|
77
|
+
setToLanguages(to)
|
|
78
|
+
const fromId = from?.id
|
|
79
|
+
const toIds = to?.map((l) => l.id) ?? []
|
|
80
|
+
const docMembers = getDocumentMembersFlat(document, documentSchema)
|
|
81
|
+
if (fromId && toIds?.length) {
|
|
82
|
+
const transMap = getTranslationMap(
|
|
83
|
+
documentSchema,
|
|
84
|
+
docMembers,
|
|
85
|
+
fromId,
|
|
86
|
+
toIds,
|
|
87
|
+
config?.translationOutputs ?? defaultLanguageOutputs
|
|
88
|
+
)
|
|
89
|
+
setTranslationMap(transMap)
|
|
90
|
+
} else {
|
|
91
|
+
setTranslationMap(undefined)
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
[config]
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const toggleToLanguage = useCallback(
|
|
98
|
+
(
|
|
99
|
+
toggledLang: Language,
|
|
100
|
+
toLanguages: Language[] | undefined,
|
|
101
|
+
languages: Language[] | undefined
|
|
102
|
+
) => {
|
|
103
|
+
if (!languages) {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
const wasSelected = !!toLanguages?.find((l) => l.id === toggledLang.id)
|
|
107
|
+
const newToLangs = languages.filter(
|
|
108
|
+
(anyLang) =>
|
|
109
|
+
!!toLanguages?.find(
|
|
110
|
+
(selectedLang) => toggledLang.id !== selectedLang.id && selectedLang.id === anyLang.id
|
|
111
|
+
) ||
|
|
112
|
+
(toggledLang.id === anyLang.id && !wasSelected)
|
|
113
|
+
)
|
|
114
|
+
setToLanguages(newToLangs)
|
|
115
|
+
},
|
|
116
|
+
[]
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const contextValue: FieldTranslationContextValue = useMemo(() => {
|
|
120
|
+
return {
|
|
121
|
+
openFieldTranslation: async (
|
|
122
|
+
document: SanityDocumentLike,
|
|
123
|
+
documentSchema: ObjectSchemaType
|
|
124
|
+
) => {
|
|
125
|
+
setDialogOpen(true)
|
|
126
|
+
const languageParams = getLanguageParams(config?.selectLanguageParams, document)
|
|
127
|
+
const languages: Language[] | undefined = await (typeof config?.languages === 'function'
|
|
128
|
+
? config?.languages(languageClient, languageParams)
|
|
129
|
+
: Promise.resolve(config?.languages))
|
|
130
|
+
setLanguages(languages)
|
|
131
|
+
setDocument(document)
|
|
132
|
+
setDocumentSchema(documentSchema)
|
|
133
|
+
const fromLanguage = languages?.[0]
|
|
134
|
+
if (fromLanguage) {
|
|
135
|
+
selectFromLanguage(fromLanguage, languages, document, documentSchema)
|
|
136
|
+
} else {
|
|
137
|
+
console.error('No languages available for selected language params', languageParams)
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
translationLoading: false,
|
|
141
|
+
}
|
|
142
|
+
}, [selectFromLanguage, config, languageClient])
|
|
143
|
+
|
|
144
|
+
const runDisabled =
|
|
145
|
+
!fromLanguage || !toLanguages?.length || !translationMap?.length || !documentId
|
|
146
|
+
|
|
147
|
+
const onRunTranslation = useCallback(() => {
|
|
148
|
+
if (translationMap && documentId) {
|
|
149
|
+
runTranslate({
|
|
150
|
+
documentId,
|
|
151
|
+
fieldLanguageMap: translationMap.map((map) => ({
|
|
152
|
+
...map,
|
|
153
|
+
// eslint-disable-next-line max-nested-callbacks
|
|
154
|
+
outputs: map.outputs.filter((out) => !!toLanguages?.find((l) => l.id === out.id)),
|
|
155
|
+
})),
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
close()
|
|
159
|
+
}, [translationMap, documentId, runTranslate, close, toLanguages])
|
|
160
|
+
|
|
161
|
+
const runButton = (
|
|
162
|
+
<Button
|
|
163
|
+
text={`Translate`}
|
|
164
|
+
tone="primary"
|
|
165
|
+
icon={PlayIcon}
|
|
166
|
+
style={{width: '100%'}}
|
|
167
|
+
disabled={runDisabled}
|
|
168
|
+
onClick={onRunTranslation}
|
|
169
|
+
/>
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<FieldTranslationContext.Provider value={contextValue}>
|
|
174
|
+
{dialogOpen ? (
|
|
175
|
+
<Dialog
|
|
176
|
+
id={id}
|
|
177
|
+
width={1}
|
|
178
|
+
open={dialogOpen}
|
|
179
|
+
onClose={close}
|
|
180
|
+
header="Translate fields"
|
|
181
|
+
footer={
|
|
182
|
+
<Flex justify="space-between" padding={2} flex={1}>
|
|
183
|
+
{runDisabled ? (
|
|
184
|
+
<Tooltip
|
|
185
|
+
content={
|
|
186
|
+
<Flex padding={2}>
|
|
187
|
+
<Text>Nothing to translate.</Text>
|
|
188
|
+
</Flex>
|
|
189
|
+
}
|
|
190
|
+
placement="top"
|
|
191
|
+
>
|
|
192
|
+
<Flex flex={1}>{runButton}</Flex>
|
|
193
|
+
</Tooltip>
|
|
194
|
+
) : (
|
|
195
|
+
runButton
|
|
196
|
+
)}
|
|
197
|
+
</Flex>
|
|
198
|
+
}
|
|
199
|
+
>
|
|
200
|
+
{languages ? (
|
|
201
|
+
<Flex padding={4} gap={5} align="flex-start" justify="center">
|
|
202
|
+
<Stack space={2}>
|
|
203
|
+
<Box marginBottom={2}>
|
|
204
|
+
<Text weight="semibold">From</Text>
|
|
205
|
+
</Box>
|
|
206
|
+
{languages?.map((l) => (
|
|
207
|
+
<Flex key={l.id} gap={3} align="center">
|
|
208
|
+
<Radio
|
|
209
|
+
name="fromLang"
|
|
210
|
+
value={l.id}
|
|
211
|
+
checked={l.id === fromLanguage?.id}
|
|
212
|
+
onClick={() => selectFromLanguage(l, languages, document, documentSchema)}
|
|
213
|
+
/>
|
|
214
|
+
<Text>{l.title ?? l.id}</Text>
|
|
215
|
+
</Flex>
|
|
216
|
+
))}
|
|
217
|
+
</Stack>
|
|
218
|
+
|
|
219
|
+
<Stack space={2}>
|
|
220
|
+
<Box marginBottom={2}>
|
|
221
|
+
<Text weight="semibold">To</Text>
|
|
222
|
+
</Box>
|
|
223
|
+
{languages
|
|
224
|
+
?.filter((l) => l.id !== fromLanguage?.id)
|
|
225
|
+
.map((l) => (
|
|
226
|
+
<Flex key={l.id} gap={3} align="center">
|
|
227
|
+
<Checkbox
|
|
228
|
+
name="toLang"
|
|
229
|
+
value={l.id}
|
|
230
|
+
checked={!!toLanguages?.find((tl) => tl.id === l.id)}
|
|
231
|
+
onClick={() => toggleToLanguage(l, toLanguages, languages)}
|
|
232
|
+
disabled={
|
|
233
|
+
!translationMap?.find((tm) => tm.outputs.find((o) => o.id === l.id))
|
|
234
|
+
}
|
|
235
|
+
/>
|
|
236
|
+
<Text>{l.title ?? l.id}</Text>
|
|
237
|
+
</Flex>
|
|
238
|
+
))}
|
|
239
|
+
</Stack>
|
|
240
|
+
</Flex>
|
|
241
|
+
) : (
|
|
242
|
+
<Flex padding={4} gap={2} align="flex-start" justify="center">
|
|
243
|
+
<Box>
|
|
244
|
+
<Spinner />
|
|
245
|
+
</Box>
|
|
246
|
+
<Text>Loading languages...</Text>
|
|
247
|
+
</Flex>
|
|
248
|
+
)}
|
|
249
|
+
</Dialog>
|
|
250
|
+
) : null}
|
|
251
|
+
{props.children}
|
|
252
|
+
</FieldTranslationContext.Provider>
|
|
253
|
+
)
|
|
254
|
+
}
|
|
@@ -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
|
+
}
|