sanity-plugin-internationalized-array
Version:
Store localized fields in an array to save on attributes
124 lines (110 loc) • 4 kB
text/typescript
/* eslint-disable no-nested-ternary */
import {defineField, type FieldDefinition, type Rule} from 'sanity'
import {peek} from '../cache'
import {createFieldName} from '../components/createFieldName'
import {getSelectedValue} from '../components/getSelectedValue'
import InternationalizedArray from '../components/InternationalizedArray'
import type {Language, LanguageCallback, Value} from '../types'
import {getLanguagesFieldOption} from '../utils/getLanguagesFieldOption'
type ArrayFactoryConfig = {
apiVersion: string
select?: Record<string, string>
languages: Language[] | LanguageCallback
defaultLanguages?: string[]
type: string | FieldDefinition
}
export type ArrayFieldOptions = Pick<
ArrayFactoryConfig,
'apiVersion' | 'select' | 'languages'
>
export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
const {apiVersion, select, languages, type} = config
const typeName = typeof type === `string` ? type : type.name
const arrayName = createFieldName(typeName)
const objectName = createFieldName(typeName, true)
return defineField({
name: arrayName,
title: 'Internationalized array',
type: 'array',
components: {
input: InternationalizedArray,
},
options: {
// @ts-expect-error - these options are required for validation rules – not the custom input component
apiVersion,
select,
languages,
},
of: [
defineField({
...(typeof type === 'string' ? {} : type),
name: objectName,
type: objectName,
}),
],
// @ts-expect-error - fix typings
validation: (rule: Rule) =>
rule.custom<Value[]>(async (value, context) => {
if (!value) {
return true
}
const selectedValue = getSelectedValue(select, context.document)
const client = context.getClient({apiVersion})
let contextLanguages: Language[] = []
const languagesFieldOption = getLanguagesFieldOption(context?.type)
if (Array.isArray(languagesFieldOption)) {
contextLanguages = languagesFieldOption
} else if (Array.isArray(peek(selectedValue))) {
contextLanguages = peek(selectedValue) || []
} else if (typeof languagesFieldOption === 'function') {
contextLanguages = await languagesFieldOption(client, selectedValue)
}
if (value && value.length > contextLanguages.length) {
return `Cannot be more than ${
contextLanguages.length === 1
? `1 item`
: `${contextLanguages.length} items`
}`
}
const nonLanguageKeys = value?.length
? value.filter(
(item) =>
!contextLanguages.find((language) => item._key === language.id)
)
: []
if (nonLanguageKeys.length) {
return {
message: `Array item keys must be valid languages registered to the field type`,
paths: nonLanguageKeys.map((item) => [{_key: item._key}]),
}
}
// Ensure there's no duplicate `language` fields
type KeyedValues = {
[key: string]: Value[]
}
const valuesByLanguage = value?.length
? value
.filter((item) => Boolean(item?._key))
.reduce((acc, cur) => {
if (acc[cur._key]) {
return {...acc, [cur._key]: [...acc[cur._key], cur]}
}
return {
...acc,
[cur._key]: [cur],
}
}, {} as KeyedValues)
: {}
const duplicateValues = Object.values(valuesByLanguage)
.filter((item) => item?.length > 1)
.flat()
if (duplicateValues.length) {
return {
message: 'There can only be one field per language',
paths: duplicateValues.map((item) => [{_key: item._key}]),
}
}
return true
}),
})
}