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.
@@ -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 CONFIG_DEFAULT: PluginConfig = {
9
- languages: [],
10
- fieldTypes: [],
11
- }
12
-
13
- export const internationalizedArray = definePlugin<PluginConfig>(
14
- (config = CONFIG_DEFAULT) => {
15
- const {
16
- apiVersion = '2022-11-27',
17
- select,
18
- languages,
19
- fieldTypes,
20
- } = {...CONFIG_DEFAULT, ...config}
21
-
22
- return {
23
- name: 'sanity-plugin-internationalized-array',
24
- // If `languages` is a callback then let's preload it
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
- ),
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
- schema: {
38
- types: [
39
- ...fieldTypes.map((type) =>
40
- array({type, apiVersion, select, languages})
41
- ),
42
- ...fieldTypes.map((type) => object({type})),
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
+ })
@@ -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
- export type ArraySchemaWithLanguageOptions = ArraySchemaType & {
112
- options: {
113
- select?: Record<string, string>
114
- languages: Language[] | LanguageCallback
115
- apiVersion: string
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
+ }
@@ -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