sanity-plugin-internationalized-array 1.7.0 → 1.9.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.
@@ -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
+ : false
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 && 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,49 +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 CONFIG_DEFAULT: PluginConfig = {
9
- languages: [],
10
- defaultLanguages: [],
11
- fieldTypes: [],
12
- }
13
-
14
- export const internationalizedArray = definePlugin<PluginConfig>(
15
- (config = CONFIG_DEFAULT) => {
16
- const {
17
- apiVersion = '2022-11-27',
18
- select,
19
- languages,
20
- fieldTypes,
21
- defaultLanguages,
22
- } = {...CONFIG_DEFAULT, ...config}
23
-
24
- return {
25
- name: 'sanity-plugin-internationalized-array',
26
- // If `languages` is a callback then let's preload it
27
- studio: Array.isArray(languages)
28
- ? undefined
29
- : {
30
- components: {
31
- layout: (props) => (
32
- <>
33
- <Preload apiVersion={apiVersion} languages={languages} />
34
- {props.renderDefault(props)}
35
- </>
36
- ),
37
- },
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
+ ),
38
35
  },
39
- schema: {
40
- types: [
41
- ...fieldTypes.map((type) =>
42
- array({type, apiVersion, select, languages, defaultLanguages})
43
- ),
44
- ...fieldTypes.map((type) => object({type})),
45
- ],
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
+ },
46
70
  },
47
- }
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
+ },
48
81
  }
49
- )
82
+ })
@@ -16,7 +16,7 @@ type ArrayFactoryConfig = {
16
16
  }
17
17
 
18
18
  export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
19
- const {apiVersion, select, languages, defaultLanguages, type} = config
19
+ const {apiVersion, select, languages, type} = config
20
20
  const typeName = typeof type === `string` ? type : type.name
21
21
  const arrayName = createFieldName(typeName)
22
22
  const objectName = createFieldName(typeName, true)
@@ -28,7 +28,8 @@ export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
28
28
  components: {
29
29
  input: InternationalizedArray,
30
30
  },
31
- options: {apiVersion, select, languages, defaultLanguages},
31
+ // These options are required for validation rules – not the custom input component
32
+ options: {apiVersion, select, languages},
32
33
  // TODO: Resolve this typing issue with the inner object
33
34
  // @ts-expect-error
34
35
  of: [
package/src/types.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import type {
2
- ArraySchemaType,
3
2
  FieldDefinition,
4
3
  Rule,
5
4
  RuleTypeConstraint,
@@ -115,13 +114,14 @@ export type PluginConfig = {
115
114
  * ```
116
115
  */
117
116
  fieldTypes: (string | RuleTypeConstraint | FieldDefinition)[]
118
- }
119
-
120
- export type ArraySchemaWithLanguageOptions = ArraySchemaType & {
121
- options: {
122
- select?: Record<string, string>
123
- languages: Language[] | LanguageCallback
124
- apiVersion: string
125
- defaultLanguages?: string[]
126
- }
117
+ /**
118
+ * Locations where the "+ EN" add language buttons are visible
119
+ * @defaultValue ['field']
120
+ * */
121
+ buttonLocations: ('field' | 'unstable__fieldAction' | 'document')[]
122
+ /**
123
+ * Show or hide the "Add missing languages" button
124
+ * @defaultValue true
125
+ * */
126
+ buttonAddAll: boolean
127
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,76 @@
1
+ import {FormInsertPatch, insert, Path, SchemaType} from 'sanity'
2
+
3
+ import {Language, Value} from '../types'
4
+ import {createValueSchemaTypeName} from './createValueSchemaTypeName'
5
+
6
+ type AddConfig = {
7
+ // New keys to add to the field
8
+ addLanguageKeys: string[]
9
+ // Schema of the current field
10
+ schemaType: SchemaType
11
+ // All languages registered in the plugin
12
+ languages: Language[]
13
+ // Languages that are currently visible
14
+ filteredLanguages: Language[]
15
+ // Current value of the internationalizedArray field
16
+ value?: Value[]
17
+ // Path to this item
18
+ path?: Path
19
+ }
20
+
21
+ export function createAddLanguagePatches(config: AddConfig): FormInsertPatch[] {
22
+ const {
23
+ addLanguageKeys,
24
+ schemaType,
25
+ languages,
26
+ filteredLanguages,
27
+ value,
28
+ path = [],
29
+ } = config
30
+
31
+ const itemBase = {_type: createValueSchemaTypeName(schemaType)}
32
+
33
+ // Create new items
34
+ const newItems =
35
+ Array.isArray(addLanguageKeys) && addLanguageKeys.length > 0
36
+ ? // Just one for this language
37
+ addLanguageKeys.map((id) => ({...itemBase, _key: id}))
38
+ : // Or one for every missing language
39
+ filteredLanguages
40
+ .filter((language) =>
41
+ value?.length ? !value.find((v) => v._key === language.id) : true
42
+ )
43
+ .map((language) => ({...itemBase, _key: language.id}))
44
+
45
+ // Insert new items in the correct order
46
+ const languagesInUse = value?.length ? value.map((v) => v) : []
47
+
48
+ const insertions = newItems.map((item) => {
49
+ // What's the original index of this language?
50
+ const languageIndex = languages.findIndex((l) => item._key === l.id)
51
+
52
+ // What languages are there beyond that index?
53
+ const remainingLanguages = languages.slice(languageIndex + 1)
54
+
55
+ // So what is the index in the current value array of the next language in the language array?
56
+ const nextLanguageIndex = languagesInUse.findIndex((l) =>
57
+ // eslint-disable-next-line max-nested-callbacks
58
+ remainingLanguages.find((r) => r.id === l._key)
59
+ )
60
+
61
+ // Keep local state up to date incase multiple insertions are being made
62
+ if (nextLanguageIndex < 0) {
63
+ languagesInUse.push(item)
64
+ } else {
65
+ languagesInUse.splice(nextLanguageIndex, 0, item)
66
+ }
67
+
68
+ return nextLanguageIndex < 0
69
+ ? // No next language (-1), add to end of array
70
+ insert([item], 'after', [...path, nextLanguageIndex])
71
+ : // Next language found, insert before that
72
+ insert([item], 'before', [...path, nextLanguageIndex])
73
+ })
74
+
75
+ return insertions
76
+ }
@@ -0,0 +1,5 @@
1
+ import {SchemaType} from 'sanity'
2
+
3
+ export function createValueSchemaTypeName(schemaType: SchemaType): string {
4
+ return `${schemaType.name}Value`
5
+ }
@@ -1,9 +0,0 @@
1
- import React from 'react'
2
-
3
- import {Language} from '../types'
4
-
5
- export const LanguageContext = React.createContext<{languages: Language[]}>({
6
- languages: [],
7
- })
8
-
9
- export const LanguageProvider = LanguageContext.Provider