sanity-plugin-internationalized-array 0.0.7 → 1.1.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/LICENSE +1 -1
- package/README.md +82 -32
- package/lib/cjs/index.js +487 -0
- package/lib/cjs/index.js.map +1 -0
- package/lib/esm/index.js +480 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/types/index.d.ts +12 -0
- package/lib/types/index.d.ts.map +1 -0
- package/package.json +50 -36
- package/sanity.json +7 -6
- package/src/components/Feedback.tsx +27 -0
- package/src/components/InternationalizedArrayInput.tsx +222 -0
- package/src/{LanguageArray → components}/Table.tsx +0 -0
- package/src/components/createFieldName.ts +20 -0
- package/src/components/getToneFromValidation.ts +18 -0
- package/src/index.ts +1 -3
- package/src/plugin.tsx +23 -0
- package/src/schema/array.ts +69 -0
- package/src/schema/object.ts +29 -0
- package/src/types.ts +19 -9
- package/v2-incompatible.js +11 -0
- package/lib/LanguageArray/Table.js +0 -88
- package/lib/LanguageArray/Table.js.map +0 -1
- package/lib/LanguageArray/ValueInput.js +0 -17
- package/lib/LanguageArray/ValueInput.js.map +0 -1
- package/lib/LanguageArray/index.js +0 -253
- package/lib/LanguageArray/index.js.map +0 -1
- package/lib/hooks/useUnsetInputComponent.js +0 -32
- package/lib/hooks/useUnsetInputComponent.js.map +0 -1
- package/lib/index.js +0 -13
- package/lib/index.js.map +0 -1
- package/lib/internationalizedArray.js +0 -105
- package/lib/internationalizedArray.js.map +0 -1
- package/lib/types.js +0 -2
- package/lib/types.js.map +0 -1
- package/migrations/transformObjectToArray.js +0 -94
- package/src/LanguageArray/ValueInput.tsx +0 -6
- package/src/LanguageArray/index.tsx +0 -311
- package/src/hooks/useUnsetInputComponent.tsx +0 -17
- package/src/internationalizedArray.ts +0 -84
package/package.json
CHANGED
|
@@ -1,57 +1,71 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sanity-plugin-internationalized-array",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Store
|
|
5
|
-
"
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Store localized fields in an array to save on attributes",
|
|
5
|
+
"author": "Simeon Griggs <simeon@sanity.io>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"source": "./src/index.ts",
|
|
8
|
+
"main": "./lib/cjs/index.js",
|
|
9
|
+
"module": "./lib/esm/index.js",
|
|
10
|
+
"types": "./lib/types/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"require": "./lib/cjs/index.js",
|
|
14
|
+
"default": "./lib/esm/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"lib",
|
|
20
|
+
"v2-incompatible.js",
|
|
21
|
+
"sanity.json"
|
|
22
|
+
],
|
|
6
23
|
"scripts": {
|
|
7
|
-
"
|
|
8
|
-
"verify": "sanipack verify",
|
|
9
|
-
"watch": "sanipack build --watch",
|
|
10
|
-
"_postinstall": "husky install",
|
|
11
|
-
"prepublishOnly": "pinst --disable && sanipack build && sanipack verify",
|
|
12
|
-
"postpublish": "pinst --enable",
|
|
24
|
+
"clean": "rimraf lib",
|
|
13
25
|
"lint": "eslint .",
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
}
|
|
26
|
+
"prebuild": "npm run clean && plugin-kit verify-package --silent",
|
|
27
|
+
"build": "parcel build --no-cache",
|
|
28
|
+
"watch": "parcel watch",
|
|
29
|
+
"link-watch": "plugin-kit link-watch",
|
|
30
|
+
"prepublishOnly": "npm run build"
|
|
20
31
|
},
|
|
21
32
|
"repository": {
|
|
22
33
|
"type": "git",
|
|
23
34
|
"url": "git+ssh://git@github.com/SimeonGriggs/sanity-plugin-internationalized-array.git"
|
|
24
35
|
},
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
],
|
|
29
|
-
"author": "Sanity.io <hello@sanity.io>",
|
|
30
|
-
"license": "MIT",
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=14.0.0"
|
|
38
|
+
},
|
|
31
39
|
"dependencies": {
|
|
32
40
|
"@sanity/icons": "^1.3.1",
|
|
33
|
-
"@sanity/
|
|
41
|
+
"@sanity/incompatible-plugin": "^0.0.1-studio-v3.1",
|
|
42
|
+
"@sanity/ui": "^0.37.12",
|
|
43
|
+
"sanity-plugin-utils": "^0.0.1",
|
|
34
44
|
"styled-components": "^5.3.5"
|
|
35
45
|
},
|
|
36
|
-
"peerDependencies": {
|
|
37
|
-
"@sanity/base": "^2.30.1",
|
|
38
|
-
"@sanity/desk-tool": "^2.30.1",
|
|
39
|
-
"@sanity/form-builder": "^2.30.1",
|
|
40
|
-
"@sanity/util": "^2.29.5",
|
|
41
|
-
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
|
42
|
-
},
|
|
43
46
|
"devDependencies": {
|
|
44
|
-
"@
|
|
45
|
-
"
|
|
47
|
+
"@parcel/packager-ts": "^2.6.2",
|
|
48
|
+
"@parcel/transformer-typescript-types": "^2.6.2",
|
|
49
|
+
"@sanity/plugin-kit": "^0.1.0-v3-studio.1",
|
|
50
|
+
"@types/styled-components": "^5.1.25",
|
|
51
|
+
"@typescript-eslint/eslint-plugin": "^5.30.7",
|
|
52
|
+
"@typescript-eslint/parser": "^5.30.7",
|
|
53
|
+
"eslint": "^8.20.0",
|
|
46
54
|
"eslint-config-prettier": "^8.5.0",
|
|
47
|
-
"eslint-config-sanity": "6.0.0",
|
|
55
|
+
"eslint-config-sanity": "^6.0.0",
|
|
48
56
|
"eslint-plugin-prettier": "^4.2.1",
|
|
49
57
|
"eslint-plugin-react": "^7.30.1",
|
|
50
|
-
"
|
|
51
|
-
"
|
|
58
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
59
|
+
"parcel": "^2.6.2",
|
|
52
60
|
"prettier": "^2.7.1",
|
|
53
|
-
"
|
|
54
|
-
"
|
|
61
|
+
"react": "^17.0.0 || ^18.0.0",
|
|
62
|
+
"rimraf": "^3.0.2",
|
|
63
|
+
"sanity": "2.29.5-purple-unicorn.856",
|
|
64
|
+
"typescript": "4.7.4"
|
|
65
|
+
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"react": "^17.0.0 || ^18.0.0",
|
|
68
|
+
"sanity": "purple-unicorn"
|
|
55
69
|
},
|
|
56
70
|
"bugs": {
|
|
57
71
|
"url": "https://github.com/SimeonGriggs/sanity-plugin-internationalized-array/issues"
|
package/sanity.json
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {Text, Card, Stack, Code} from '@sanity/ui'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
const schemaExample = {
|
|
5
|
+
languages: [
|
|
6
|
+
{id: 'en', title: 'English'},
|
|
7
|
+
{id: 'no', title: 'Norsk'},
|
|
8
|
+
],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function Feedback() {
|
|
12
|
+
return (
|
|
13
|
+
<Card tone="caution" border radius={2} padding={3}>
|
|
14
|
+
<Stack space={4}>
|
|
15
|
+
<Text>
|
|
16
|
+
An array of language objects must be passed into the <code>internationalizedArray</code>{' '}
|
|
17
|
+
helper function, each with an <code>id</code> and <code>title</code> field. Example:
|
|
18
|
+
</Text>
|
|
19
|
+
<Card padding={2} border radius={2}>
|
|
20
|
+
<Code size={1} language="javascript">
|
|
21
|
+
{JSON.stringify(schemaExample, null, 2)}
|
|
22
|
+
</Code>
|
|
23
|
+
</Card>
|
|
24
|
+
</Stack>
|
|
25
|
+
</Card>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import React, {useCallback, useMemo} from 'react'
|
|
2
|
+
import {
|
|
3
|
+
ArrayOfObjectsInputProps,
|
|
4
|
+
MemberItem,
|
|
5
|
+
unset,
|
|
6
|
+
insert,
|
|
7
|
+
set,
|
|
8
|
+
setIfMissing,
|
|
9
|
+
FormFieldValidationStatus,
|
|
10
|
+
} from 'sanity/form'
|
|
11
|
+
import {Box, Button, Flex, Grid, Label, Stack} from '@sanity/ui'
|
|
12
|
+
|
|
13
|
+
import {Language, Value, ArraySchemaWithLanguageOptions} from '../types'
|
|
14
|
+
import {Table, TableCell, TableRow} from './Table'
|
|
15
|
+
import {AddIcon, RemoveIcon, RestoreIcon} from '@sanity/icons'
|
|
16
|
+
import Feedback from './Feedback'
|
|
17
|
+
import {getToneFromValidation} from './getToneFromValidation'
|
|
18
|
+
|
|
19
|
+
export type InternationalizedArrayInputProps = ArrayOfObjectsInputProps<
|
|
20
|
+
Value,
|
|
21
|
+
ArraySchemaWithLanguageOptions
|
|
22
|
+
>
|
|
23
|
+
|
|
24
|
+
export default function InternationalizedArrayInput(props: InternationalizedArrayInputProps) {
|
|
25
|
+
const {members, value, schemaType, onChange} = props
|
|
26
|
+
const readOnly = typeof schemaType.readOnly === 'boolean' ? schemaType.readOnly : false
|
|
27
|
+
const {options} = schemaType
|
|
28
|
+
|
|
29
|
+
const languages: Language[] = useMemo(() => options?.languages ?? [], [options])
|
|
30
|
+
|
|
31
|
+
const handleAddLanguage = useCallback(
|
|
32
|
+
(languageId?: string) => {
|
|
33
|
+
// Create new items
|
|
34
|
+
const newItems = languageId
|
|
35
|
+
? // Just one for this language
|
|
36
|
+
[{_key: languageId}]
|
|
37
|
+
: // Or one for every missing language
|
|
38
|
+
languages
|
|
39
|
+
.filter((language) =>
|
|
40
|
+
value?.length ? !value.find((v) => v._key === language.id) : true
|
|
41
|
+
)
|
|
42
|
+
.map((language) => ({_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', [nextLanguageIndex])
|
|
70
|
+
: // Next language found, insert before that
|
|
71
|
+
insert([item], 'before', [nextLanguageIndex])
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
onChange([setIfMissing([]), ...insertions])
|
|
75
|
+
},
|
|
76
|
+
[languages, onChange, value]
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const handleUnsetByKey = useCallback(
|
|
80
|
+
(_key: string) => {
|
|
81
|
+
onChange(unset([{_key}]))
|
|
82
|
+
},
|
|
83
|
+
[onChange]
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
// TODO: This is lazy, reordering and re-setting the whole array – it could be surgical
|
|
87
|
+
const handleRestoreOrder = useCallback(() => {
|
|
88
|
+
if (!value?.length) {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Create a new value array in the correct order
|
|
93
|
+
// This would also strip out values that don't have a language as the key
|
|
94
|
+
const updatedValue = value
|
|
95
|
+
.reduce((acc, v) => {
|
|
96
|
+
const newIndex = languages.findIndex((l) => l.id === v?._key)
|
|
97
|
+
|
|
98
|
+
if (newIndex) {
|
|
99
|
+
acc[newIndex] = v
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return acc
|
|
103
|
+
}, [] as Value[])
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
|
|
106
|
+
onChange(set(updatedValue))
|
|
107
|
+
}, [languages, onChange, value])
|
|
108
|
+
|
|
109
|
+
const allKeysAreLanguages = useMemo(() => {
|
|
110
|
+
return value?.every((v) => languages.find((l) => l?.id === v?._key))
|
|
111
|
+
}, [value, languages])
|
|
112
|
+
|
|
113
|
+
// Check languages are in the correct order
|
|
114
|
+
const languagesOutOfOrder = useMemo(() => {
|
|
115
|
+
if (!value?.length) {
|
|
116
|
+
return []
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const languagesInUse = languages.filter((l) => value.find((v) => v._key === l.id))
|
|
120
|
+
|
|
121
|
+
return value
|
|
122
|
+
.map((v, vIndex) => (vIndex === languagesInUse.findIndex((l) => l.id === v._key) ? null : v))
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
}, [value, languages])
|
|
125
|
+
|
|
126
|
+
const languagesAreValid = useMemo(
|
|
127
|
+
() =>
|
|
128
|
+
!languages?.length || (languages?.length && languages.every((item) => item.id && item.title)),
|
|
129
|
+
[languages]
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if (!languagesAreValid) {
|
|
133
|
+
return <Feedback />
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<Stack space={2}>
|
|
138
|
+
{members?.length > 0 ? (
|
|
139
|
+
<Table>
|
|
140
|
+
<tbody>
|
|
141
|
+
{members.map((member) => (
|
|
142
|
+
<TableRow
|
|
143
|
+
key={member.key}
|
|
144
|
+
tone={
|
|
145
|
+
member?.item?.validation?.length > 0
|
|
146
|
+
? getToneFromValidation(member.item.validation)
|
|
147
|
+
: undefined
|
|
148
|
+
}
|
|
149
|
+
>
|
|
150
|
+
<TableCell style={{verticalAlign: 'bottom'}}>
|
|
151
|
+
<Box paddingY={3} paddingRight={2}>
|
|
152
|
+
<Label muted size={1}>
|
|
153
|
+
{member.key}
|
|
154
|
+
</Label>
|
|
155
|
+
</Box>
|
|
156
|
+
</TableCell>
|
|
157
|
+
<TableCell paddingRight={2} style={{width: `100%`}}>
|
|
158
|
+
{/* This renders the entire field default with title */}
|
|
159
|
+
<MemberItem {...props} member={member} />
|
|
160
|
+
</TableCell>
|
|
161
|
+
<TableCell style={{verticalAlign: 'bottom'}}>
|
|
162
|
+
<Flex align="center" justify="flex-end" gap={3}>
|
|
163
|
+
{/* Possibly unncessary, validation shows up in <MemberItem /> */}
|
|
164
|
+
{member?.item?.validation?.length > 0 ? (
|
|
165
|
+
<Box paddingLeft={2}>
|
|
166
|
+
<FormFieldValidationStatus validation={member.item.validation} />
|
|
167
|
+
</Box>
|
|
168
|
+
) : null}
|
|
169
|
+
<Button
|
|
170
|
+
mode="ghost"
|
|
171
|
+
icon={RemoveIcon}
|
|
172
|
+
tone="critical"
|
|
173
|
+
disabled={typeof readOnly === 'boolean' ? readOnly : false}
|
|
174
|
+
onClick={() => handleUnsetByKey(member.key)}
|
|
175
|
+
/>
|
|
176
|
+
</Flex>
|
|
177
|
+
</TableCell>
|
|
178
|
+
</TableRow>
|
|
179
|
+
))}
|
|
180
|
+
</tbody>
|
|
181
|
+
</Table>
|
|
182
|
+
) : null}
|
|
183
|
+
|
|
184
|
+
{languagesOutOfOrder.length > 0 && allKeysAreLanguages ? (
|
|
185
|
+
<Button
|
|
186
|
+
tone="caution"
|
|
187
|
+
icon={RestoreIcon}
|
|
188
|
+
onClick={() => handleRestoreOrder()}
|
|
189
|
+
text="Restore order of languages"
|
|
190
|
+
/>
|
|
191
|
+
) : null}
|
|
192
|
+
|
|
193
|
+
{languages?.length > 0 ? (
|
|
194
|
+
<Stack space={2}>
|
|
195
|
+
{/* No more than 5 columns */}
|
|
196
|
+
<Grid columns={Math.min(languages.length, 5)} gap={2}>
|
|
197
|
+
{languages.map((language) => (
|
|
198
|
+
<Button
|
|
199
|
+
key={language.id}
|
|
200
|
+
tone="primary"
|
|
201
|
+
mode="ghost"
|
|
202
|
+
fontSize={1}
|
|
203
|
+
disabled={readOnly || Boolean(value?.find((item) => item._key === language.id))}
|
|
204
|
+
text={language.id.toUpperCase()}
|
|
205
|
+
icon={AddIcon}
|
|
206
|
+
onClick={() => handleAddLanguage(language.id)}
|
|
207
|
+
/>
|
|
208
|
+
))}
|
|
209
|
+
</Grid>
|
|
210
|
+
<Button
|
|
211
|
+
tone="primary"
|
|
212
|
+
mode="ghost"
|
|
213
|
+
disabled={readOnly || (value && value?.length >= languages?.length)}
|
|
214
|
+
icon={AddIcon}
|
|
215
|
+
text={value?.length ? `Add missing languages` : `Add all languages`}
|
|
216
|
+
onClick={() => handleAddLanguage()}
|
|
217
|
+
/>
|
|
218
|
+
</Stack>
|
|
219
|
+
) : null}
|
|
220
|
+
</Stack>
|
|
221
|
+
)
|
|
222
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function camelCase(string: string) {
|
|
2
|
+
return string.replace(/-([a-z])/g, (g) => g[1].toUpperCase())
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function titleCase(string: string) {
|
|
6
|
+
return string
|
|
7
|
+
.split(` `)
|
|
8
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
9
|
+
.join(` `)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function pascalCase(string: string) {
|
|
13
|
+
return titleCase(camelCase(string))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createFieldName(name: string, addValue = false): string {
|
|
17
|
+
return addValue
|
|
18
|
+
? [`internationalizedArray`, pascalCase(name), `Value`].join(``)
|
|
19
|
+
: [`internationalizedArray`, pascalCase(name)].join(``)
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {NodeValidation} from 'sanity/form'
|
|
2
|
+
import {CardTone} from '@sanity/ui'
|
|
3
|
+
|
|
4
|
+
export function getToneFromValidation(validations: NodeValidation[]): CardTone {
|
|
5
|
+
if (!validations.length) {
|
|
6
|
+
return `default`
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const validationLevels = validations.map((v) => v.level)
|
|
10
|
+
|
|
11
|
+
if (validationLevels.includes('error')) {
|
|
12
|
+
return `critical`
|
|
13
|
+
} else if (validationLevels.includes('warning')) {
|
|
14
|
+
return `caution`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return `default`
|
|
18
|
+
}
|
package/src/index.ts
CHANGED
package/src/plugin.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {createPlugin} from 'sanity'
|
|
2
|
+
import {PluginConfig} from './types'
|
|
3
|
+
import array from './schema/array'
|
|
4
|
+
import object from './schema/object'
|
|
5
|
+
|
|
6
|
+
const CONFIG_DEFAULT = {
|
|
7
|
+
languages: [],
|
|
8
|
+
fieldTypes: [],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const internationalizedArray = createPlugin<PluginConfig>((config = CONFIG_DEFAULT) => {
|
|
12
|
+
const {languages, fieldTypes} = {...CONFIG_DEFAULT, ...config}
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
name: 'sanity-plugin-internationalized-array',
|
|
16
|
+
schema: {
|
|
17
|
+
types: [
|
|
18
|
+
...fieldTypes.map((type) => array({type, languages})),
|
|
19
|
+
...fieldTypes.map((type) => object({type})),
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
})
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {defineField, Rule, Schema} from 'sanity'
|
|
2
|
+
|
|
3
|
+
import {createFieldName} from '../components/createFieldName'
|
|
4
|
+
import InternationalizedArrayInput from '../components/InternationalizedArrayInput'
|
|
5
|
+
import {Language, Value} from '../types'
|
|
6
|
+
|
|
7
|
+
type ArrayFactoryConfig = {
|
|
8
|
+
languages: Language[]
|
|
9
|
+
type: string | Schema.FieldDefinition
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default (config: ArrayFactoryConfig): Schema.FieldDefinition<'array'> => {
|
|
13
|
+
const {languages, type} = config
|
|
14
|
+
const typeName = typeof type === `string` ? type : type.name
|
|
15
|
+
const arrayName = createFieldName(typeName)
|
|
16
|
+
const objectName = createFieldName(typeName, true)
|
|
17
|
+
|
|
18
|
+
return defineField({
|
|
19
|
+
name: arrayName,
|
|
20
|
+
title: 'Internationalized array',
|
|
21
|
+
type: 'array',
|
|
22
|
+
components: {input: InternationalizedArrayInput},
|
|
23
|
+
options: {languages},
|
|
24
|
+
of: [defineField({name: objectName, type: objectName})],
|
|
25
|
+
validation: (rule: Rule) =>
|
|
26
|
+
rule.max(languages?.length).custom<Value[]>((value, context) => {
|
|
27
|
+
const {languages: contextLanguages}: {languages: Language[]} = context?.type?.options ?? {}
|
|
28
|
+
const nonLanguageKeys = value?.length
|
|
29
|
+
? value.filter((item) => !contextLanguages.find((language) => item._key === language.id))
|
|
30
|
+
: []
|
|
31
|
+
if (nonLanguageKeys.length) {
|
|
32
|
+
return {
|
|
33
|
+
message: `Array item keys must be valid languages registered to the field type`,
|
|
34
|
+
paths: nonLanguageKeys.map((item) => [{_key: item._key}]),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Ensure there's no duplicate `language` fields
|
|
39
|
+
type KeyedValues = {
|
|
40
|
+
[key: string]: Value[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const valuesByLanguage = value?.length
|
|
44
|
+
? value
|
|
45
|
+
.filter((item) => Boolean(item?._key))
|
|
46
|
+
.reduce((acc, cur) => {
|
|
47
|
+
if (acc[cur._key]) {
|
|
48
|
+
return {...acc, [cur._key]: [...acc[cur._key], cur]}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
...acc,
|
|
52
|
+
[cur._key]: [cur],
|
|
53
|
+
}
|
|
54
|
+
}, {} as KeyedValues)
|
|
55
|
+
: {}
|
|
56
|
+
const duplicateValues = Object.values(valuesByLanguage)
|
|
57
|
+
.filter((item) => item?.length > 1)
|
|
58
|
+
.flat()
|
|
59
|
+
if (duplicateValues.length) {
|
|
60
|
+
return {
|
|
61
|
+
message: 'There can only be one field per language',
|
|
62
|
+
paths: duplicateValues.map((item) => [{_key: item._key}]),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return true
|
|
67
|
+
}),
|
|
68
|
+
})
|
|
69
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {defineField, Schema} from 'sanity'
|
|
2
|
+
|
|
3
|
+
import {createFieldName} from '../components/createFieldName'
|
|
4
|
+
|
|
5
|
+
type ObjectFactoryConfig = {
|
|
6
|
+
type: string | Schema.FieldDefinition
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default (config: ObjectFactoryConfig): Schema.FieldDefinition<'object'> => {
|
|
10
|
+
const {type} = config
|
|
11
|
+
const typeName = typeof type === `string` ? type : type.name
|
|
12
|
+
const objectName = createFieldName(typeName, true)
|
|
13
|
+
|
|
14
|
+
return defineField({
|
|
15
|
+
name: objectName,
|
|
16
|
+
title: `Internationalized array ${type}`,
|
|
17
|
+
type: 'object',
|
|
18
|
+
fields: [
|
|
19
|
+
typeof type === `string`
|
|
20
|
+
? // Define a basic field if all we have is the string name
|
|
21
|
+
defineField({
|
|
22
|
+
name: 'value',
|
|
23
|
+
type,
|
|
24
|
+
})
|
|
25
|
+
: // Pass in the configured options, but overwrite the name
|
|
26
|
+
{...type, name: 'value'},
|
|
27
|
+
],
|
|
28
|
+
})
|
|
29
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
|
-
import {Rule} from '
|
|
1
|
+
import {Rule, ArraySchemaType, Schema} from 'sanity'
|
|
2
2
|
|
|
3
|
-
export type
|
|
3
|
+
export type Language = {
|
|
4
|
+
id: string
|
|
5
|
+
title: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type AllowedType = 'string' | 'number' | 'boolean' | 'text' | 'reference'
|
|
9
|
+
|
|
10
|
+
export type ArrayConfig = {
|
|
4
11
|
name: string
|
|
5
|
-
type:
|
|
12
|
+
type: AllowedType
|
|
13
|
+
languages: Language[]
|
|
6
14
|
title?: string
|
|
7
15
|
group?: string
|
|
8
16
|
hidden?: boolean | (() => boolean)
|
|
9
17
|
readOnly?: boolean | (() => boolean)
|
|
10
18
|
validation?: Rule | Rule[]
|
|
19
|
+
field?: {[key: string]: any; options: {[key: string]: any}}
|
|
11
20
|
}
|
|
12
21
|
|
|
13
22
|
export type Value = {
|
|
@@ -15,12 +24,13 @@ export type Value = {
|
|
|
15
24
|
value?: string
|
|
16
25
|
}
|
|
17
26
|
|
|
18
|
-
export type
|
|
19
|
-
|
|
20
|
-
|
|
27
|
+
export type PluginConfig = {
|
|
28
|
+
languages: Language[]
|
|
29
|
+
fieldTypes: (string | Schema.FieldDefinition)[]
|
|
21
30
|
}
|
|
22
31
|
|
|
23
|
-
export type
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
export type ArraySchemaWithLanguageOptions = ArraySchemaType & {
|
|
33
|
+
options: {
|
|
34
|
+
languages: Language[]
|
|
35
|
+
}
|
|
26
36
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const {showIncompatiblePluginDialog} = require('@sanity/incompatible-plugin')
|
|
2
|
+
const {name, version, sanityExchangeUrl} = require('./package.json')
|
|
3
|
+
|
|
4
|
+
export default showIncompatiblePluginDialog({
|
|
5
|
+
name: name,
|
|
6
|
+
versions: {
|
|
7
|
+
v3: version,
|
|
8
|
+
v2: undefined,
|
|
9
|
+
},
|
|
10
|
+
sanityExchangeUrl,
|
|
11
|
+
})
|