sanity-plugin-internationalized-array 1.6.2 → 1.8.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.
- package/README.md +135 -7
- package/lib/index.d.ts +19 -9
- package/lib/index.esm.js +21 -12
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +21 -12
- package/lib/index.js.map +1 -1
- package/package.json +5 -3
- package/src/components/InternationalizedArray.tsx +159 -160
- package/src/components/InternationalizedArrayContext.tsx +91 -0
- package/src/components/InternationalizedInput.tsx +8 -5
- package/src/constants.ts +13 -0
- package/src/fieldActions/index.ts +132 -0
- package/src/plugin.tsx +73 -38
- package/src/schema/array.ts +2 -0
- package/src/types.ts +19 -9
- package/src/utils/checkAllLanguagesArePresent.ts +14 -0
- package/src/utils/createAddAllTitle.ts +16 -0
- package/src/utils/createAddLanguagePatches.ts +75 -0
- package/src/components/languageContext.tsx +0 -9
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {AddIcon, TranslateIcon} from '@sanity/icons'
|
|
2
|
+
import {useCallback} from 'react'
|
|
3
|
+
import {
|
|
4
|
+
defineDocumentFieldAction,
|
|
5
|
+
DocumentFieldActionItem,
|
|
6
|
+
DocumentFieldActionProps,
|
|
7
|
+
PatchEvent,
|
|
8
|
+
setIfMissing,
|
|
9
|
+
useFormValue,
|
|
10
|
+
} from 'sanity'
|
|
11
|
+
import {useDocumentPane} from 'sanity/desk'
|
|
12
|
+
|
|
13
|
+
import {useInternationalizedArrayContext} from '../components/InternationalizedArrayContext'
|
|
14
|
+
import {Language, Value} from '../types'
|
|
15
|
+
import {checkAllLanguagesArePresent} from '../utils/checkAllLanguagesArePresent'
|
|
16
|
+
import {createAddAllTitle} from '../utils/createAddAllTitle'
|
|
17
|
+
import {createAddLanguagePatches} from '../utils/createAddLanguagePatches'
|
|
18
|
+
|
|
19
|
+
const createTranslateFieldActions: (
|
|
20
|
+
fieldActionProps: DocumentFieldActionProps,
|
|
21
|
+
context: {languages: Language[]; filteredLanguages: Language[]}
|
|
22
|
+
) => DocumentFieldActionItem[] = (
|
|
23
|
+
fieldActionProps,
|
|
24
|
+
{languages, filteredLanguages}
|
|
25
|
+
) =>
|
|
26
|
+
languages.map((language) => {
|
|
27
|
+
const value = useFormValue(fieldActionProps.path) as Value[]
|
|
28
|
+
const disabled =
|
|
29
|
+
value && Array.isArray(value)
|
|
30
|
+
? Boolean(value?.find((item) => item._key === language.id))
|
|
31
|
+
: true
|
|
32
|
+
const hidden = !filteredLanguages.some((f) => f.id === language.id)
|
|
33
|
+
|
|
34
|
+
const {onChange} = useDocumentPane()
|
|
35
|
+
|
|
36
|
+
const onAction = useCallback(() => {
|
|
37
|
+
const {schemaType, path} = fieldActionProps
|
|
38
|
+
|
|
39
|
+
const addLanguageKeys = [language.id]
|
|
40
|
+
const patches = createAddLanguagePatches({
|
|
41
|
+
addLanguageKeys,
|
|
42
|
+
schemaType,
|
|
43
|
+
languages,
|
|
44
|
+
filteredLanguages,
|
|
45
|
+
value,
|
|
46
|
+
path,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
onChange(PatchEvent.from([setIfMissing([], path), ...patches]))
|
|
50
|
+
}, [language.id, value, onChange])
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
type: 'action',
|
|
54
|
+
icon: AddIcon,
|
|
55
|
+
onAction,
|
|
56
|
+
title: language.id.toLocaleUpperCase(),
|
|
57
|
+
hidden,
|
|
58
|
+
disabled,
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const AddMissingTranslationsFieldAction: (
|
|
63
|
+
fieldActionProps: DocumentFieldActionProps,
|
|
64
|
+
context: {languages: Language[]; filteredLanguages: Language[]}
|
|
65
|
+
) => DocumentFieldActionItem = (
|
|
66
|
+
fieldActionProps,
|
|
67
|
+
{languages, filteredLanguages}
|
|
68
|
+
) => {
|
|
69
|
+
const value = useFormValue(fieldActionProps.path) as Value[]
|
|
70
|
+
const disabled = value.length === filteredLanguages.length
|
|
71
|
+
const hidden = checkAllLanguagesArePresent(filteredLanguages, value)
|
|
72
|
+
|
|
73
|
+
const {onChange} = useDocumentPane()
|
|
74
|
+
|
|
75
|
+
const onAction = useCallback(() => {
|
|
76
|
+
const {schemaType, path} = fieldActionProps
|
|
77
|
+
|
|
78
|
+
const addLanguageKeys: string[] = []
|
|
79
|
+
const patches = createAddLanguagePatches({
|
|
80
|
+
addLanguageKeys,
|
|
81
|
+
schemaType,
|
|
82
|
+
languages,
|
|
83
|
+
filteredLanguages,
|
|
84
|
+
value,
|
|
85
|
+
path,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
onChange(PatchEvent.from([setIfMissing([], path), ...patches]))
|
|
89
|
+
}, [fieldActionProps, filteredLanguages, languages, onChange, value])
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
type: 'action',
|
|
93
|
+
icon: AddIcon,
|
|
94
|
+
onAction,
|
|
95
|
+
title: createAddAllTitle(value, filteredLanguages),
|
|
96
|
+
disabled,
|
|
97
|
+
hidden,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const internationalizedArrayFieldAction = defineDocumentFieldAction({
|
|
102
|
+
name: 'internationalizedArray',
|
|
103
|
+
useAction(fieldActionProps) {
|
|
104
|
+
const isInternationalizedArrayField =
|
|
105
|
+
fieldActionProps?.schemaType?.type?.name.startsWith(
|
|
106
|
+
'internationalizedArray'
|
|
107
|
+
)
|
|
108
|
+
const {languages, filteredLanguages} = useInternationalizedArrayContext()
|
|
109
|
+
|
|
110
|
+
const translateFieldActions = createTranslateFieldActions(
|
|
111
|
+
fieldActionProps,
|
|
112
|
+
{languages, filteredLanguages}
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
type: 'group',
|
|
117
|
+
icon: TranslateIcon,
|
|
118
|
+
title: 'Add Translation',
|
|
119
|
+
renderAsButton: true,
|
|
120
|
+
children: isInternationalizedArrayField
|
|
121
|
+
? [
|
|
122
|
+
...translateFieldActions,
|
|
123
|
+
AddMissingTranslationsFieldAction(fieldActionProps, {
|
|
124
|
+
languages,
|
|
125
|
+
filteredLanguages,
|
|
126
|
+
}),
|
|
127
|
+
]
|
|
128
|
+
: [],
|
|
129
|
+
hidden: !isInternationalizedArrayField,
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
})
|
package/src/plugin.tsx
CHANGED
|
@@ -1,47 +1,82 @@
|
|
|
1
|
-
import {definePlugin} from 'sanity'
|
|
1
|
+
import {definePlugin, isObjectInputProps} from 'sanity'
|
|
2
2
|
|
|
3
|
+
import {InternationalizedArrayProvider} from './components/InternationalizedArrayContext'
|
|
3
4
|
import Preload from './components/Preload'
|
|
5
|
+
import {CONFIG_DEFAULT} from './constants'
|
|
6
|
+
import {internationalizedArrayFieldAction} from './fieldActions'
|
|
4
7
|
import array from './schema/array'
|
|
5
8
|
import object from './schema/object'
|
|
6
9
|
import {PluginConfig} from './types'
|
|
7
10
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
{props.renderDefault(props)}
|
|
33
|
-
</>
|
|
34
|
-
),
|
|
35
|
-
},
|
|
11
|
+
export const internationalizedArray = definePlugin<PluginConfig>((config) => {
|
|
12
|
+
const pluginConfig = {...CONFIG_DEFAULT, ...config}
|
|
13
|
+
const {
|
|
14
|
+
apiVersion = '2022-11-27',
|
|
15
|
+
select,
|
|
16
|
+
languages,
|
|
17
|
+
fieldTypes,
|
|
18
|
+
defaultLanguages,
|
|
19
|
+
buttonLocations,
|
|
20
|
+
} = pluginConfig
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
name: 'sanity-plugin-internationalized-array',
|
|
24
|
+
// Preload languages for use throughout the Studio
|
|
25
|
+
studio: Array.isArray(languages)
|
|
26
|
+
? undefined
|
|
27
|
+
: {
|
|
28
|
+
components: {
|
|
29
|
+
layout: (props) => (
|
|
30
|
+
<>
|
|
31
|
+
<Preload apiVersion={apiVersion} languages={languages} />
|
|
32
|
+
{props.renderDefault(props)}
|
|
33
|
+
</>
|
|
34
|
+
),
|
|
36
35
|
},
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
},
|
|
37
|
+
// Optional: render "add language" buttons as field actions
|
|
38
|
+
document: {
|
|
39
|
+
unstable_fieldActions: buttonLocations.includes('unstable__fieldAction')
|
|
40
|
+
? (prev) => [...prev, internationalizedArrayFieldAction]
|
|
41
|
+
: undefined,
|
|
42
|
+
},
|
|
43
|
+
// Wrap document editor with a language provider
|
|
44
|
+
form: {
|
|
45
|
+
components: {
|
|
46
|
+
input: (props) => {
|
|
47
|
+
const isRootInput = props.id === 'root' && isObjectInputProps(props)
|
|
48
|
+
|
|
49
|
+
if (!isRootInput) {
|
|
50
|
+
return props.renderDefault(props)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const rootFieldTypeNames = props.schemaType.fields.map(
|
|
54
|
+
(field) => field.type.name
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const hasInternationalizedArray = rootFieldTypeNames.some((name) =>
|
|
58
|
+
name.startsWith('internationalizedArray')
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if (!hasInternationalizedArray) {
|
|
62
|
+
return props.renderDefault(props)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return InternationalizedArrayProvider({
|
|
66
|
+
...props,
|
|
67
|
+
internationalizedArray: pluginConfig,
|
|
68
|
+
})
|
|
69
|
+
},
|
|
44
70
|
},
|
|
45
|
-
}
|
|
71
|
+
},
|
|
72
|
+
// Register custom schema types for the outer array and the inner object
|
|
73
|
+
schema: {
|
|
74
|
+
types: [
|
|
75
|
+
...fieldTypes.map((type) =>
|
|
76
|
+
array({type, apiVersion, select, languages, defaultLanguages})
|
|
77
|
+
),
|
|
78
|
+
...fieldTypes.map((type) => object({type})),
|
|
79
|
+
],
|
|
80
|
+
},
|
|
46
81
|
}
|
|
47
|
-
)
|
|
82
|
+
})
|
package/src/schema/array.ts
CHANGED
|
@@ -11,6 +11,7 @@ type ArrayFactoryConfig = {
|
|
|
11
11
|
apiVersion: string
|
|
12
12
|
select?: Record<string, string>
|
|
13
13
|
languages: Language[] | LanguageCallback
|
|
14
|
+
defaultLanguages?: string[]
|
|
14
15
|
type: string | FieldDefinition
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -27,6 +28,7 @@ export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
|
|
|
27
28
|
components: {
|
|
28
29
|
input: InternationalizedArray,
|
|
29
30
|
},
|
|
31
|
+
// These options are required for validation rules – not the custom input component
|
|
30
32
|
options: {apiVersion, select, languages},
|
|
31
33
|
// TODO: Resolve this typing issue with the inner object
|
|
32
34
|
// @ts-expect-error
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
ArraySchemaType,
|
|
3
2
|
FieldDefinition,
|
|
4
3
|
Rule,
|
|
5
4
|
RuleTypeConstraint,
|
|
@@ -82,6 +81,15 @@ export type PluginConfig = {
|
|
|
82
81
|
* ```
|
|
83
82
|
*/
|
|
84
83
|
languages: Language[] | LanguageCallback
|
|
84
|
+
/**
|
|
85
|
+
* You can specify a list of language IDs that should be pre-filled when creating a new document
|
|
86
|
+
* ```tsx
|
|
87
|
+
* {
|
|
88
|
+
* defaultLanguages: ['en']
|
|
89
|
+
* }
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
defaultLanguages?: string[]
|
|
85
93
|
/**
|
|
86
94
|
* Can be a string matching core field types, as well as custom ones:
|
|
87
95
|
* ```tsx
|
|
@@ -106,12 +114,14 @@ export type PluginConfig = {
|
|
|
106
114
|
* ```
|
|
107
115
|
*/
|
|
108
116
|
fieldTypes: (string | RuleTypeConstraint | FieldDefinition)[]
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Locations where the "+ EN" add language buttons are visible
|
|
119
|
+
* @defaultValue ['field']
|
|
120
|
+
* */
|
|
121
|
+
buttonLocations: ('field' | 'unstable__fieldAction')[]
|
|
122
|
+
/**
|
|
123
|
+
* Show or hide the "Add missing languages" button
|
|
124
|
+
* @defaultValue true
|
|
125
|
+
* */
|
|
126
|
+
buttonAddAll: boolean
|
|
117
127
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {Language, Value} from '../types'
|
|
2
|
+
|
|
3
|
+
export function checkAllLanguagesArePresent(
|
|
4
|
+
languages: Language[],
|
|
5
|
+
value: Value[] | undefined
|
|
6
|
+
): boolean {
|
|
7
|
+
const filteredLanguageIds = languages.map((l) => l.id)
|
|
8
|
+
const languagesInUseIds = value ? value.map((v) => v._key) : []
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
languagesInUseIds.length === filteredLanguageIds.length &&
|
|
12
|
+
languagesInUseIds.every((l) => filteredLanguageIds.includes(l))
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {Language, Value} from '../types'
|
|
2
|
+
|
|
3
|
+
export function createAddAllTitle(
|
|
4
|
+
value: Value[] | undefined,
|
|
5
|
+
languages: Language[]
|
|
6
|
+
): string {
|
|
7
|
+
if (value?.length) {
|
|
8
|
+
return `Add missing ${
|
|
9
|
+
languages.length - value.length === 1 ? `language` : `languages`
|
|
10
|
+
}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return languages.length === 1
|
|
14
|
+
? `Add ${languages[0].title} Field`
|
|
15
|
+
: `Add all languages`
|
|
16
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {FormInsertPatch, insert, Path, SchemaType} from 'sanity'
|
|
2
|
+
|
|
3
|
+
import {Language, Value} from '../types'
|
|
4
|
+
|
|
5
|
+
type AddConfig = {
|
|
6
|
+
// New keys to add to the field
|
|
7
|
+
addLanguageKeys: string[]
|
|
8
|
+
// Schema of the current field
|
|
9
|
+
schemaType: SchemaType
|
|
10
|
+
// All languages registered in the plugin
|
|
11
|
+
languages: Language[]
|
|
12
|
+
// Languages that are currently visible
|
|
13
|
+
filteredLanguages: Language[]
|
|
14
|
+
// Current value of the internationalizedArray field
|
|
15
|
+
value?: Value[]
|
|
16
|
+
// Path to this item
|
|
17
|
+
path?: Path
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createAddLanguagePatches(config: AddConfig): FormInsertPatch[] {
|
|
21
|
+
const {
|
|
22
|
+
addLanguageKeys,
|
|
23
|
+
schemaType,
|
|
24
|
+
languages,
|
|
25
|
+
filteredLanguages,
|
|
26
|
+
value,
|
|
27
|
+
path = [],
|
|
28
|
+
} = config
|
|
29
|
+
|
|
30
|
+
const itemBase = {_type: `${schemaType.name}Value`}
|
|
31
|
+
|
|
32
|
+
// Create new items
|
|
33
|
+
const newItems =
|
|
34
|
+
Array.isArray(addLanguageKeys) && addLanguageKeys.length > 0
|
|
35
|
+
? // Just one for this language
|
|
36
|
+
addLanguageKeys.map((id) => ({...itemBase, _key: id}))
|
|
37
|
+
: // Or one for every missing language
|
|
38
|
+
filteredLanguages
|
|
39
|
+
.filter((language) =>
|
|
40
|
+
value?.length ? !value.find((v) => v._key === language.id) : true
|
|
41
|
+
)
|
|
42
|
+
.map((language) => ({...itemBase, _key: language.id}))
|
|
43
|
+
|
|
44
|
+
// Insert new items in the correct order
|
|
45
|
+
const languagesInUse = value?.length ? value.map((v) => v) : []
|
|
46
|
+
|
|
47
|
+
const insertions = newItems.map((item) => {
|
|
48
|
+
// What's the original index of this language?
|
|
49
|
+
const languageIndex = languages.findIndex((l) => item._key === l.id)
|
|
50
|
+
|
|
51
|
+
// What languages are there beyond that index?
|
|
52
|
+
const remainingLanguages = languages.slice(languageIndex + 1)
|
|
53
|
+
|
|
54
|
+
// So what is the index in the current value array of the next language in the language array?
|
|
55
|
+
const nextLanguageIndex = languagesInUse.findIndex((l) =>
|
|
56
|
+
// eslint-disable-next-line max-nested-callbacks
|
|
57
|
+
remainingLanguages.find((r) => r.id === l._key)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
// Keep local state up to date incase multiple insertions are being made
|
|
61
|
+
if (nextLanguageIndex < 0) {
|
|
62
|
+
languagesInUse.push(item)
|
|
63
|
+
} else {
|
|
64
|
+
languagesInUse.splice(nextLanguageIndex, 0, item)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return nextLanguageIndex < 0
|
|
68
|
+
? // No next language (-1), add to end of array
|
|
69
|
+
insert([item], 'after', [...path, nextLanguageIndex])
|
|
70
|
+
: // Next language found, insert before that
|
|
71
|
+
insert([item], 'before', [...path, nextLanguageIndex])
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
return insertions
|
|
75
|
+
}
|