sanity-plugin-internationalized-array 1.6.2 → 1.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanity-plugin-internationalized-array",
3
- "version": "1.6.2",
3
+ "version": "1.7.0",
4
4
  "description": "Store localized fields in an array to save on attributes",
5
5
  "keywords": [
6
6
  "sanity",
@@ -51,6 +51,7 @@
51
51
  "dependencies": {
52
52
  "@sanity/icons": "^2.2.2",
53
53
  "@sanity/incompatible-plugin": "^1.0.4",
54
+ "@sanity/language-filter": "^3.1.2",
54
55
  "@sanity/ui": "^1.2.2",
55
56
  "fast-deep-equal": "^3.1.3",
56
57
  "lodash.get": "^4.4.2",
@@ -97,7 +98,8 @@
97
98
  },
98
99
  "sanityPlugin": {
99
100
  "verifyPackage": {
100
- "eslintImports": false
101
+ "eslintImports": false,
102
+ "dependencies": false
101
103
  }
102
104
  }
103
105
  }
@@ -1,20 +1,22 @@
1
1
  import {AddIcon} from '@sanity/icons'
2
+ import {useLanguageFilterStudioContext} from '@sanity/language-filter'
2
3
  import {Button, Grid, Stack, useToast} from '@sanity/ui'
3
4
  import equal from 'fast-deep-equal'
4
- import {useCallback, useDeferredValue, useEffect, useMemo} from 'react'
5
+ import React, {useCallback, useDeferredValue, useEffect, useMemo} from 'react'
5
6
  import {
6
7
  ArrayOfObjectsInputProps,
7
8
  ArrayOfObjectsItem,
8
- ArrayOfObjectsItemMember,
9
9
  insert,
10
10
  set,
11
11
  setIfMissing,
12
12
  useClient,
13
13
  useFormBuilder,
14
+ useFormValue,
14
15
  } from 'sanity'
15
16
  import {suspend} from 'suspend-react'
16
17
 
17
18
  import {namespace, version} from '../cache'
19
+ import {MAX_COLUMNS} from '../constants'
18
20
  import type {ArraySchemaWithLanguageOptions, Value} from '../types'
19
21
  import Feedback from './Feedback'
20
22
  import {getSelectedValue} from './getSelectedValue'
@@ -30,6 +32,7 @@ export default function InternationalizedArray(
30
32
  props: InternationalizedArrayProps
31
33
  ) {
32
34
  const {members, value, schemaType, onChange} = props
35
+
33
36
  const readOnly =
34
37
  typeof schemaType.readOnly === 'boolean' ? schemaType.readOnly : false
35
38
  const {options} = schemaType
@@ -41,7 +44,7 @@ export default function InternationalizedArray(
41
44
  [options.select, deferredDocument]
42
45
  )
43
46
 
44
- const {apiVersion} = options
47
+ const {apiVersion, defaultLanguages} = options
45
48
  const client = useClient({apiVersion})
46
49
  const languages = Array.isArray(options.languages)
47
50
  ? options.languages
@@ -57,24 +60,76 @@ export default function InternationalizedArray(
57
60
  {equal}
58
61
  )
59
62
 
63
+ // Support updating the UI if languageFilter is installed
64
+ const {selectedLanguageIds, options: languageFilterOptions} =
65
+ useLanguageFilterStudioContext()
66
+ const documentType = useFormValue(['_type'])
67
+ const languageFilterEnabled =
68
+ typeof documentType === 'string' &&
69
+ languageFilterOptions.documentTypes.includes(documentType)
70
+
71
+ const filteredMembers = useMemo(
72
+ () =>
73
+ languageFilterEnabled
74
+ ? members.filter((member) => {
75
+ // This member is the outer object created by the plugin
76
+ // Satisfy TS
77
+ if (member.kind !== 'item') {
78
+ return false
79
+ }
80
+
81
+ // This is the inner "value" field member created by this plugin
82
+ const valueMember = member.item.members[0]
83
+
84
+ // Satisfy TS
85
+ if (valueMember.kind !== 'field') {
86
+ return false
87
+ }
88
+
89
+ return languageFilterOptions.filterField(
90
+ member.item.schemaType,
91
+ valueMember,
92
+ selectedLanguageIds
93
+ )
94
+ })
95
+ : members,
96
+ [languageFilterEnabled, members, languageFilterOptions, selectedLanguageIds]
97
+ )
98
+
99
+ const filteredLanguages = useMemo(
100
+ () =>
101
+ languageFilterEnabled
102
+ ? languages.filter((language) =>
103
+ selectedLanguageIds.includes(language.id)
104
+ )
105
+ : languages,
106
+ [languageFilterEnabled, languages, selectedLanguageIds]
107
+ )
108
+
60
109
  const handleAddLanguage = useCallback(
61
- (languageId?: string) => {
62
- if (!languages?.length) {
110
+ (param?: React.MouseEvent<HTMLButtonElement, MouseEvent> | string[]) => {
111
+ if (!filteredLanguages?.length) {
63
112
  return
64
113
  }
65
114
 
115
+ const languageIds: string[] = Array.isArray(param)
116
+ ? param
117
+ : ([param?.currentTarget?.value].filter(Boolean) as string[])
66
118
  const itemBase = {_type: `${schemaType.name}Value`}
67
119
 
68
120
  // Create new items
69
- const newItems = languageId
70
- ? // Just one for this language
71
- [{...itemBase, _key: languageId}]
72
- : // Or one for every missing language
73
- languages
74
- .filter((language) =>
75
- value?.length ? !value.find((v) => v._key === language.id) : true
76
- )
77
- .map((language) => ({...itemBase, _key: language.id}))
121
+ const newItems =
122
+ Array.isArray(languageIds) && languageIds.length > 0
123
+ ? // Just one for this language
124
+ languageIds.map((id) => ({...itemBase, _key: id}))
125
+ : // Or one for every missing language
126
+ filteredLanguages
127
+ .filter((language) =>
128
+ value?.length
129
+ ? !value.find((v) => v._key === language.id)
130
+ : true
131
+ )
132
+ .map((language) => ({...itemBase, _key: language.id}))
78
133
 
79
134
  // Insert new items in the correct order
80
135
  const languagesInUse = value?.length ? value.map((v) => v) : []
@@ -108,10 +163,25 @@ export default function InternationalizedArray(
108
163
 
109
164
  onChange([setIfMissing([]), ...insertions])
110
165
  },
111
- [languages, onChange, schemaType.name, value]
166
+ [filteredLanguages, onChange, schemaType.name, value]
112
167
  )
113
168
 
114
- // TODO: This is lazy, reordering and re-setting the whole array it could be surgical
169
+ // Create default fields if the document is not yet created
170
+ const documentCreatedAt = useFormValue(['_createdAt'])
171
+
172
+ if (
173
+ // Array field is empty
174
+ !value &&
175
+ // Document form is in "not yet created" state
176
+ !documentCreatedAt &&
177
+ // Plugin config included default languages
178
+ defaultLanguages &&
179
+ defaultLanguages?.length > 0
180
+ ) {
181
+ handleAddLanguage(defaultLanguages)
182
+ }
183
+
184
+ // TODO: This is reordering and re-setting the whole array, it could be surgical
115
185
  const handleRestoreOrder = useCallback(() => {
116
186
  if (!value?.length || !languages?.length) {
117
187
  return
@@ -177,24 +247,34 @@ export default function InternationalizedArray(
177
247
  [languages]
178
248
  )
179
249
 
250
+ // Automatically restore order of fields
180
251
  useEffect(() => {
181
252
  if (languagesOutOfOrder.length > 0 && allKeysAreLanguages) {
182
253
  handleRestoreOrder()
183
254
  }
184
255
  }, [languagesOutOfOrder, allKeysAreLanguages, handleRestoreOrder])
185
256
 
257
+ // compare value keys with possible languages
258
+ const allLanguagesArePresent = useMemo(() => {
259
+ const filteredLanguageIds = filteredLanguages.map((l) => l.id)
260
+ const languagesInUseIds = value ? value.map((v) => v._key) : []
261
+
262
+ return (
263
+ languagesInUseIds.length === filteredLanguageIds.length &&
264
+ languagesInUseIds.every((l) => filteredLanguageIds.includes(l))
265
+ )
266
+ }, [filteredLanguages, value])
267
+
186
268
  if (!languagesAreValid) {
187
269
  return <Feedback />
188
270
  }
189
271
 
190
272
  return (
191
- <LanguageProvider value={{languages}}>
273
+ <LanguageProvider value={{languages: filteredLanguages}}>
192
274
  <Stack space={2}>
193
275
  {members?.length > 0 ? (
194
276
  <>
195
- {/* TODO: Resolve type for ArrayOfObjectsItemMember */}
196
- {/* @ts-ignore */}
197
- {members.map((member: ArrayOfObjectsItemMember) => {
277
+ {filteredMembers.map((member) => {
198
278
  if (member.kind === 'item') {
199
279
  return (
200
280
  <ArrayOfObjectsItem
@@ -213,25 +293,18 @@ export default function InternationalizedArray(
213
293
  </>
214
294
  ) : null}
215
295
 
216
- {/* This now happens automatically */}
217
- {/* {languagesOutOfOrder.length > 0 && allKeysAreLanguages ? (
218
- <Button
219
- tone="caution"
220
- icon={RestoreIcon}
221
- onClick={() => handleRestoreOrder()}
222
- text="Restore order of languages"
223
- />
224
- ) : null} */}
225
-
226
296
  {/* Show buttons if languages are configured */}
227
297
  {/* Hide them if all languages have values */}
228
- {languages?.length > 0 && languagesInUse.length < languages.length ? (
298
+ {filteredLanguages?.length > 0 && !allLanguagesArePresent ? (
229
299
  <Stack space={2}>
230
300
  {/* Hide language-specific buttons if there's only one */}
231
- {/* No more than 5 columns */}
232
- {languages.length > 1 ? (
233
- <Grid columns={Math.min(languages.length, 5)} gap={2}>
234
- {languages.map((language) => (
301
+ {/* No more than 7 columns */}
302
+ {filteredLanguages.length > 1 ? (
303
+ <Grid
304
+ columns={Math.min(filteredLanguages.length, MAX_COLUMNS)}
305
+ gap={2}
306
+ >
307
+ {filteredLanguages.map((language) => (
235
308
  <Button
236
309
  key={language.id}
237
310
  tone="primary"
@@ -242,8 +315,14 @@ export default function InternationalizedArray(
242
315
  Boolean(value?.find((item) => item._key === language.id))
243
316
  }
244
317
  text={language.id.toUpperCase()}
245
- icon={AddIcon}
246
- onClick={() => handleAddLanguage(language.id)}
318
+ // Only show plus icon if there's one row or less
319
+ icon={
320
+ filteredLanguages.length > MAX_COLUMNS
321
+ ? undefined
322
+ : AddIcon
323
+ }
324
+ value={language.id}
325
+ onClick={handleAddLanguage}
247
326
  />
248
327
  ))}
249
328
  </Grid>
@@ -251,23 +330,21 @@ export default function InternationalizedArray(
251
330
  <Button
252
331
  tone="primary"
253
332
  mode="ghost"
254
- disabled={
255
- readOnly || (value && value?.length >= languages?.length)
256
- }
333
+ disabled={readOnly || allLanguagesArePresent}
257
334
  icon={AddIcon}
258
335
  text={
259
336
  // eslint-disable-next-line no-nested-ternary
260
337
  value?.length
261
338
  ? `Add missing ${
262
- languages.length - value.length === 1
339
+ filteredLanguages.length - value.length === 1
263
340
  ? `language`
264
341
  : `languages`
265
342
  }`
266
- : languages.length === 1
267
- ? `Add ${languages[0].title} Field`
343
+ : filteredLanguages.length === 1
344
+ ? `Add ${filteredLanguages[0].title} Field`
268
345
  : `Add all languages`
269
346
  }
270
- onClick={() => handleAddLanguage()}
347
+ onClick={handleAddLanguage}
271
348
  />
272
349
  </Stack>
273
350
  ) : null}
@@ -10,7 +10,7 @@ import {
10
10
  Spinner,
11
11
  Stack,
12
12
  } from '@sanity/ui'
13
- import React, {useCallback, useMemo} from 'react'
13
+ import React, {useCallback, useContext, useMemo} from 'react'
14
14
  import {ObjectItemProps, useFormValue} from 'sanity'
15
15
  import {set, unset} from 'sanity'
16
16
 
@@ -44,7 +44,7 @@ export default function InternationalizedInput(
44
44
  const {validation, value, onChange, readOnly} = inlineProps
45
45
 
46
46
  // The parent array contains the languages from the plugin config
47
- const {languages} = React.useContext(LanguageContext)
47
+ const {languages} = useContext(LanguageContext)
48
48
 
49
49
  const languageKeysInUse = useMemo(
50
50
  () => parentValue?.map((v) => v._key) ?? [],
@@ -56,7 +56,9 @@ export default function InternationalizedInput(
56
56
 
57
57
  // Changes the key of this item, ideally to a valid language
58
58
  const handleKeyChange = useCallback(
59
- (languageId: string) => {
59
+ (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
60
+ const languageId = event?.currentTarget?.value
61
+
60
62
  if (
61
63
  !value ||
62
64
  !languages?.length ||
@@ -99,12 +101,13 @@ export default function InternationalizedInput(
99
101
  fontSize={1}
100
102
  key={language.id}
101
103
  text={language.id.toLocaleUpperCase()}
102
- onClick={() => handleKeyChange(language.id)}
104
+ value={language.id}
105
+ // @ts-expect-error
106
+ onClick={handleKeyChange}
103
107
  />
104
108
  ))}
105
109
  </Menu>
106
110
  }
107
- placement="right"
108
111
  popover={{portal: true}}
109
112
  />
110
113
  )}
@@ -0,0 +1 @@
1
+ export const MAX_COLUMNS = 7
package/src/plugin.tsx CHANGED
@@ -7,6 +7,7 @@ import {PluginConfig} from './types'
7
7
 
8
8
  const CONFIG_DEFAULT: PluginConfig = {
9
9
  languages: [],
10
+ defaultLanguages: [],
10
11
  fieldTypes: [],
11
12
  }
12
13
 
@@ -17,6 +18,7 @@ export const internationalizedArray = definePlugin<PluginConfig>(
17
18
  select,
18
19
  languages,
19
20
  fieldTypes,
21
+ defaultLanguages,
20
22
  } = {...CONFIG_DEFAULT, ...config}
21
23
 
22
24
  return {
@@ -37,7 +39,7 @@ export const internationalizedArray = definePlugin<PluginConfig>(
37
39
  schema: {
38
40
  types: [
39
41
  ...fieldTypes.map((type) =>
40
- array({type, apiVersion, select, languages})
42
+ array({type, apiVersion, select, languages, defaultLanguages})
41
43
  ),
42
44
  ...fieldTypes.map((type) => object({type})),
43
45
  ],
@@ -11,11 +11,12 @@ 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
 
17
18
  export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
18
- const {apiVersion, select, languages, type} = config
19
+ const {apiVersion, select, languages, defaultLanguages, type} = config
19
20
  const typeName = typeof type === `string` ? type : type.name
20
21
  const arrayName = createFieldName(typeName)
21
22
  const objectName = createFieldName(typeName, true)
@@ -27,7 +28,7 @@ export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
27
28
  components: {
28
29
  input: InternationalizedArray,
29
30
  },
30
- options: {apiVersion, select, languages},
31
+ options: {apiVersion, select, languages, defaultLanguages},
31
32
  // TODO: Resolve this typing issue with the inner object
32
33
  // @ts-expect-error
33
34
  of: [
package/src/types.ts CHANGED
@@ -82,6 +82,15 @@ export type PluginConfig = {
82
82
  * ```
83
83
  */
84
84
  languages: Language[] | LanguageCallback
85
+ /**
86
+ * You can specify a list of language IDs that should be pre-filled when creating a new document
87
+ * ```tsx
88
+ * {
89
+ * defaultLanguages: ['en']
90
+ * }
91
+ * ```
92
+ */
93
+ defaultLanguages?: string[]
85
94
  /**
86
95
  * Can be a string matching core field types, as well as custom ones:
87
96
  * ```tsx
@@ -113,5 +122,6 @@ export type ArraySchemaWithLanguageOptions = ArraySchemaType & {
113
122
  select?: Record<string, string>
114
123
  languages: Language[] | LanguageCallback
115
124
  apiVersion: string
125
+ defaultLanguages?: string[]
116
126
  }
117
127
  }