sanity-plugin-internationalized-array 1.1.2 → 1.2.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.
@@ -1,61 +1,45 @@
1
1
  import React, {useCallback, useMemo} from 'react'
2
2
  import {
3
- ArrayOfObjectsInputProps,
4
- ArrayOfObjectsItem,
5
- unset,
6
3
  insert,
7
4
  set,
8
5
  setIfMissing,
9
- } from 'sanity/form'
10
- import {
11
- Text,
12
- Box,
13
- Button,
14
- Flex,
15
- Grid,
16
- Label,
17
- MenuButton,
18
- Stack,
19
- useToast,
20
- Menu,
21
- MenuItem,
22
- } from '@sanity/ui'
23
- import {AddIcon, RemoveIcon, RestoreIcon} from '@sanity/icons'
6
+ ArrayOfObjectsItemMember,
7
+ ArrayOfObjectsItem,
8
+ ArrayOfObjectsInputProps,
9
+ } from 'sanity'
10
+ import {Button, Grid, Stack, useToast} from '@sanity/ui'
11
+ import {AddIcon, RestoreIcon} from '@sanity/icons'
24
12
 
25
13
  import {Language, Value, ArraySchemaWithLanguageOptions} from '../types'
26
- import {Table, TableCell, TableRow} from './Table'
27
14
  import Feedback from './Feedback'
28
- import {getToneFromValidation} from './getToneFromValidation'
29
15
 
30
- export type InternationalizedArrayInputProps = ArrayOfObjectsInputProps<
16
+ export type InternationalizedArrayProps = ArrayOfObjectsInputProps<
31
17
  Value,
32
18
  ArraySchemaWithLanguageOptions
33
19
  >
34
20
 
35
- export default function InternationalizedArrayInput(props: InternationalizedArrayInputProps) {
21
+ export default function InternationalizedArray(props: InternationalizedArrayProps) {
36
22
  const {members, value, schemaType, onChange} = props
37
23
  const readOnly = typeof schemaType.readOnly === 'boolean' ? schemaType.readOnly : false
38
24
  const {options} = schemaType
39
25
  const toast = useToast()
40
26
 
41
27
  const languages: Language[] = useMemo(() => options?.languages ?? [], [options])
42
- const valueLanguageKeys = useMemo(
43
- () => value?.map((v) => v._key).filter((key) => languages.find((l) => l.id === key)) ?? [],
44
- [languages, value]
45
- )
46
28
 
47
29
  const handleAddLanguage = useCallback(
48
30
  (languageId?: string) => {
31
+ const itemBase = {_type: `${schemaType.name}Value`}
32
+
49
33
  // Create new items
50
34
  const newItems = languageId
51
35
  ? // Just one for this language
52
- [{_key: languageId}]
36
+ [{...itemBase, _key: languageId}]
53
37
  : // Or one for every missing language
54
38
  languages
55
39
  .filter((language) =>
56
40
  value?.length ? !value.find((v) => v._key === language.id) : true
57
41
  )
58
- .map((language) => ({_key: language.id}))
42
+ .map((language) => ({...itemBase, _key: language.id}))
59
43
 
60
44
  // Insert new items in the correct order
61
45
  const languagesInUse = value?.length ? value.map((v) => v) : []
@@ -92,13 +76,6 @@ export default function InternationalizedArrayInput(props: InternationalizedArra
92
76
  [languages, onChange, value]
93
77
  )
94
78
 
95
- const handleUnsetByKey = useCallback(
96
- (_key: string) => {
97
- onChange(unset([{_key}]))
98
- },
99
- [onChange]
100
- )
101
-
102
79
  // TODO: This is lazy, reordering and re-setting the whole array – it could be surgical
103
80
  const handleRestoreOrder = useCallback(() => {
104
81
  if (!value?.length) {
@@ -119,7 +96,7 @@ export default function InternationalizedArrayInput(props: InternationalizedArra
119
96
  }, [] as Value[])
120
97
  .filter(Boolean)
121
98
 
122
- if (value.length !== updatedValue.length) {
99
+ if (value?.length !== updatedValue.length) {
123
100
  toast.push({
124
101
  title: 'There was an error reordering languages',
125
102
  status: 'warning',
@@ -134,33 +111,16 @@ export default function InternationalizedArrayInput(props: InternationalizedArra
134
111
  }, [value, languages])
135
112
 
136
113
  // Check languages are in the correct order
114
+ const languagesInUse = languages.filter((l) => value?.find((v) => v._key === l.id))
137
115
  const languagesOutOfOrder = useMemo(() => {
138
116
  if (!value?.length) {
139
117
  return []
140
118
  }
141
119
 
142
- const languagesInUse = languages.filter((l) => value.find((v) => v._key === l.id))
143
-
144
120
  return value
145
121
  .map((v, vIndex) => (vIndex === languagesInUse.findIndex((l) => l.id === v._key) ? null : v))
146
122
  .filter(Boolean)
147
- }, [value, languages])
148
-
149
- const handleKeyChange = useCallback(
150
- (config: {from: string; to: string; index: number}) => {
151
- if (!value) {
152
- return
153
- }
154
-
155
- const {from, to, index} = config
156
- const currentValue = value.find((v) => v._key === from)
157
- const newValue = {...currentValue, _key: to}
158
-
159
- // TODO: Make sure this gets the correct language index, currently replaces-in-place
160
- onChange([insert([newValue], 'after', [index]), unset([{_key: from}])])
161
- },
162
- [onChange, value]
163
- )
123
+ }, [value, languagesInUse])
164
124
 
165
125
  const languagesAreValid = useMemo(
166
126
  () =>
@@ -175,77 +135,27 @@ export default function InternationalizedArrayInput(props: InternationalizedArra
175
135
  return (
176
136
  <Stack space={2}>
177
137
  {members?.length > 0 ? (
178
- <Table>
179
- <tbody>
180
- {members.map((member, memberIndex) =>
181
- member.kind === 'item' ? (
182
- <TableRow
138
+ <>
139
+ {/* TODO: Resolve type for ArrayOfObjectsItemMember */}
140
+ {/* @ts-ignore */}
141
+ {members.map((member: ArrayOfObjectsItemMember) => {
142
+ if (member.kind === 'item') {
143
+ return (
144
+ <ArrayOfObjectsItem
183
145
  key={member.key}
184
- tone={
185
- member?.item?.validation?.length > 0
186
- ? getToneFromValidation(member.item.validation)
187
- : undefined
188
- }
189
- >
190
- <TableCell style={{verticalAlign: 'bottom'}}>
191
- <Box paddingY={3} paddingRight={2}>
192
- {valueLanguageKeys.includes(member.key) ? (
193
- <Label muted size={1}>
194
- {member.key}
195
- </Label>
196
- ) : (
197
- <MenuButton
198
- button={<Button fontSize={1} text={`Change "${member.key}"`} />}
199
- id={`${member.key}-change-key`}
200
- menu={
201
- <Menu>
202
- {languages.map((language) => (
203
- <MenuItem
204
- disabled={valueLanguageKeys.includes(language.id)}
205
- fontSize={1}
206
- key={language.id}
207
- text={language.id.toLocaleUpperCase()}
208
- onClick={() =>
209
- handleKeyChange({
210
- from: member.key,
211
- to: language.id,
212
- index: memberIndex,
213
- })
214
- }
215
- />
216
- ))}
217
- </Menu>
218
- }
219
- placement="right"
220
- popover={{portal: true}}
221
- />
222
- )}
223
- </Box>
224
- </TableCell>
225
- <TableCell paddingRight={2} style={{width: `100%`}}>
226
- {/* This renders the entire field default with title */}
227
- <ArrayOfObjectsItem {...props} member={member} />
228
- </TableCell>
229
- <TableCell style={{verticalAlign: 'bottom'}}>
230
- <Flex align="center" justify="flex-end" gap={3}>
231
- <Button
232
- mode="ghost"
233
- icon={RemoveIcon}
234
- tone="critical"
235
- disabled={typeof readOnly === 'boolean' ? readOnly : false}
236
- onClick={() => handleUnsetByKey(member.key)}
237
- />
238
- </Flex>
239
- </TableCell>
240
- </TableRow>
241
- ) : (
242
- <Text>Error</Text>
146
+ member={member}
147
+ renderItem={props.renderItem}
148
+ renderField={props.renderField}
149
+ renderInput={props.renderInput}
150
+ renderPreview={props.renderPreview}
151
+ />
243
152
  )
244
- )}
245
- </tbody>
246
- </Table>
247
- ) : null}
153
+ }
248
154
 
155
+ return null
156
+ })}
157
+ </>
158
+ ) : null}
249
159
  {languagesOutOfOrder.length > 0 && allKeysAreLanguages ? (
250
160
  <Button
251
161
  tone="caution"
@@ -255,7 +165,9 @@ export default function InternationalizedArrayInput(props: InternationalizedArra
255
165
  />
256
166
  ) : null}
257
167
 
258
- {languages?.length > 0 ? (
168
+ {/* Show buttons if languages are configured */}
169
+ {/* Hide them once languages have values */}
170
+ {languages?.length > 0 && languagesInUse.length < languages.length ? (
259
171
  <Stack space={2}>
260
172
  {/* No more than 5 columns */}
261
173
  <Grid columns={Math.min(languages.length, 5)} gap={2}>
@@ -277,7 +189,11 @@ export default function InternationalizedArrayInput(props: InternationalizedArra
277
189
  mode="ghost"
278
190
  disabled={readOnly || (value && value?.length >= languages?.length)}
279
191
  icon={AddIcon}
280
- text={value?.length ? `Add missing languages` : `Add all languages`}
192
+ text={
193
+ value?.length
194
+ ? `Add missing ${languages.length - value.length === 1 ? `language` : `languages`}`
195
+ : `Add all languages`
196
+ }
281
197
  onClick={() => handleAddLanguage()}
282
198
  />
283
199
  </Stack>
@@ -0,0 +1,108 @@
1
+ import {ObjectItemProps, useFormValue} from 'sanity'
2
+ import React, {useCallback, useMemo} from 'react'
3
+ import {unset, set} from 'sanity'
4
+ import {Box, Button, Flex, Label, MenuButton, Menu, MenuItem, Card} from '@sanity/ui'
5
+ import {RemoveIcon} from '@sanity/icons'
6
+
7
+ import {Language} from '../types'
8
+ import {getToneFromValidation} from './getToneFromValidation'
9
+
10
+ type InternationalizedValue = {
11
+ _type: string
12
+ _key: string
13
+ value: string
14
+ }
15
+
16
+ export default function InternationalizedInput(props: ObjectItemProps<InternationalizedValue>) {
17
+ const parentValue = useFormValue(props.path.slice(0, -1)) as InternationalizedValue[]
18
+
19
+ const inlineProps = {
20
+ ...props.inputProps,
21
+ // This is the magic that makes inline editing work
22
+ members: props.inputProps.members.filter((m) => m.kind === 'field' && m.name === 'value'),
23
+ // This just overrides the type
24
+ // TODO: Remove this as it shouldn't be necessary
25
+ value: props.value as InternationalizedValue,
26
+ }
27
+
28
+ const {validation, value, onChange, readOnly} = inlineProps
29
+
30
+ // The parent array contains the languages from the plugin config
31
+ // TODO: fix TS support for overloading options
32
+ const languages: Language[] = useMemo(
33
+ // @ts-ignore
34
+ () => props?.parentSchemaType?.options?.languages ?? [],
35
+ // @ts-ignore
36
+ [props?.parentSchemaType?.options?.languages]
37
+ )
38
+ const languageKeysInUse = useMemo(() => parentValue?.map((v) => v._key) ?? [], [parentValue])
39
+ const keyIsValid = languages.find((l) => l.id === value._key)
40
+
41
+ // Changes the key of this item, ideally to a valid language
42
+ const handleKeyChange = useCallback(
43
+ (languageId: string) => {
44
+ if (!value || !languages.find((l) => l.id === languageId)) {
45
+ return
46
+ }
47
+
48
+ onChange([set(languageId, ['_key'])])
49
+ },
50
+ [onChange, value, languages]
51
+ )
52
+
53
+ // Removes this item from the array
54
+ const handleUnset = useCallback(() => {
55
+ onChange(unset())
56
+ }, [onChange])
57
+
58
+ return (
59
+ <Card tone={getToneFromValidation(validation)}>
60
+ <Flex align="flex-end" gap={1}>
61
+ <Card tone="inherit">
62
+ <Box paddingY={3} paddingRight={2}>
63
+ {keyIsValid ? (
64
+ <Label muted size={1}>
65
+ {value._key}
66
+ </Label>
67
+ ) : (
68
+ <MenuButton
69
+ button={<Button fontSize={1} text={`Change "${value._key}"`} />}
70
+ id={`${value._key}-change-key`}
71
+ menu={
72
+ <Menu>
73
+ {languages.map((language) => (
74
+ <MenuItem
75
+ // TODO: Prevent changing to a key that already exists in the array
76
+ disabled={languageKeysInUse.includes(language.id)}
77
+ fontSize={1}
78
+ key={language.id}
79
+ text={language.id.toLocaleUpperCase()}
80
+ onClick={() => handleKeyChange(language.id)}
81
+ />
82
+ ))}
83
+ </Menu>
84
+ }
85
+ placement="right"
86
+ popover={{portal: true}}
87
+ />
88
+ )}
89
+ </Box>
90
+ </Card>
91
+
92
+ <Card paddingRight={2} flex={1} tone="inherit">
93
+ {props.inputProps.renderInput(props.inputProps)}
94
+ </Card>
95
+
96
+ <Card tone="inherit">
97
+ <Button
98
+ mode="ghost"
99
+ icon={RemoveIcon}
100
+ tone="critical"
101
+ disabled={readOnly}
102
+ onClick={handleUnset}
103
+ />
104
+ </Card>
105
+ </Flex>
106
+ </Card>
107
+ )
108
+ }
@@ -1,9 +1,9 @@
1
- import {NodeValidation} from 'sanity/form'
2
1
  import {CardTone} from '@sanity/ui'
2
+ import {FormNodeValidation} from 'sanity'
3
3
 
4
- export function getToneFromValidation(validations: NodeValidation[]): CardTone {
5
- if (!validations.length) {
6
- return `default`
4
+ export function getToneFromValidation(validations: FormNodeValidation[]): CardTone | undefined {
5
+ if (!validations?.length) {
6
+ return undefined
7
7
  }
8
8
 
9
9
  const validationLevels = validations.map((v) => v.level)
@@ -14,5 +14,5 @@ export function getToneFromValidation(validations: NodeValidation[]): CardTone {
14
14
  return `caution`
15
15
  }
16
16
 
17
- return `default`
17
+ return undefined
18
18
  }
package/src/plugin.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import {createPlugin} from 'sanity'
1
+ import {definePlugin} from 'sanity'
2
2
  import {PluginConfig} from './types'
3
3
  import array from './schema/array'
4
4
  import object from './schema/object'
@@ -8,7 +8,7 @@ const CONFIG_DEFAULT = {
8
8
  fieldTypes: [],
9
9
  }
10
10
 
11
- export const internationalizedArray = createPlugin<PluginConfig>((config = CONFIG_DEFAULT) => {
11
+ export const internationalizedArray = definePlugin<PluginConfig>((config = CONFIG_DEFAULT) => {
12
12
  const {languages, fieldTypes} = {...CONFIG_DEFAULT, ...config}
13
13
 
14
14
  return {
@@ -1,15 +1,15 @@
1
- import {defineField, Rule, Schema} from 'sanity'
1
+ import {defineField, FieldDefinition, Rule} from 'sanity'
2
2
 
3
3
  import {createFieldName} from '../components/createFieldName'
4
- import InternationalizedArrayInput from '../components/InternationalizedArrayInput'
4
+ import InternationalizedArray from '../components/InternationalizedArray'
5
5
  import {Language, Value} from '../types'
6
6
 
7
7
  type ArrayFactoryConfig = {
8
8
  languages: Language[]
9
- type: string | Schema.FieldDefinition
9
+ type: string | FieldDefinition
10
10
  }
11
11
 
12
- export default (config: ArrayFactoryConfig): Schema.FieldDefinition<'array'> => {
12
+ export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
13
13
  const {languages, type} = config
14
14
  const typeName = typeof type === `string` ? type : type.name
15
15
  const arrayName = createFieldName(typeName)
@@ -19,11 +19,20 @@ export default (config: ArrayFactoryConfig): Schema.FieldDefinition<'array'> =>
19
19
  name: arrayName,
20
20
  title: 'Internationalized array',
21
21
  type: 'array',
22
- components: {input: InternationalizedArrayInput},
22
+ // TODO: Resolve this typing issue with the outer component
23
+ // @ts-ignore
24
+ components: {
25
+ input: InternationalizedArray,
26
+ },
23
27
  options: {languages},
24
- // TODO: Address this typing issue with the inner object
28
+ // TODO: Resolve this typing issue with the inner object
25
29
  // @ts-ignore
26
- of: [defineField({name: objectName, type: objectName})],
30
+ of: [
31
+ defineField({
32
+ name: objectName,
33
+ type: objectName,
34
+ }),
35
+ ],
27
36
  validation: (rule: Rule) =>
28
37
  rule.max(languages?.length).custom<Value[]>((value, context) => {
29
38
  const {languages: contextLanguages}: {languages: Language[]} = context?.type?.options ?? {}
@@ -1,12 +1,13 @@
1
- import {defineField, Schema} from 'sanity'
1
+ import {defineField, FieldDefinition} from 'sanity'
2
2
 
3
3
  import {createFieldName} from '../components/createFieldName'
4
+ import InternationalizedInput from '../components/InternationalizedInput'
4
5
 
5
6
  type ObjectFactoryConfig = {
6
- type: string | Schema.FieldDefinition
7
+ type: string | FieldDefinition
7
8
  }
8
9
 
9
- export default (config: ObjectFactoryConfig): Schema.FieldDefinition<'object'> => {
10
+ export default (config: ObjectFactoryConfig): FieldDefinition<'object'> => {
10
11
  const {type} = config
11
12
  const typeName = typeof type === `string` ? type : type.name
12
13
  const objectName = createFieldName(typeName, true)
@@ -15,6 +16,14 @@ export default (config: ObjectFactoryConfig): Schema.FieldDefinition<'object'> =
15
16
  name: objectName,
16
17
  title: `Internationalized array ${type}`,
17
18
  type: 'object',
19
+ // TODO: Resolve this typing issue with the return type
20
+ // @ts-ignore
21
+ components: {
22
+ // item: InternationalizedInputWrapper,
23
+ // TODO: Resolve this typing issue with the outer component
24
+ // @ts-ignore
25
+ item: InternationalizedInput,
26
+ },
18
27
  // TODO: Address this typing issue with the inner object
19
28
  // @ts-ignore
20
29
  fields: [
@@ -27,5 +36,11 @@ export default (config: ObjectFactoryConfig): Schema.FieldDefinition<'object'> =
27
36
  : // Pass in the configured options, but overwrite the name
28
37
  {...type, name: 'value'},
29
38
  ],
39
+ preview: {
40
+ select: {
41
+ title: 'value',
42
+ subtitle: '_key',
43
+ },
44
+ },
30
45
  })
31
46
  }
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import {Rule, ArraySchemaType, Schema} from 'sanity'
1
+ import {Rule, ArraySchemaType, RuleTypeConstraint} from 'sanity'
2
2
 
3
3
  export type Language = {
4
4
  id: string
@@ -26,7 +26,7 @@ export type Value = {
26
26
 
27
27
  export type PluginConfig = {
28
28
  languages: Language[]
29
- fieldTypes: (string | Schema.FieldDefinition)[]
29
+ fieldTypes: (string | RuleTypeConstraint)[]
30
30
  }
31
31
 
32
32
  export type ArraySchemaWithLanguageOptions = ArraySchemaType & {