sanity-plugin-internationalized-array 1.4.1 → 1.6.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,16 +1,127 @@
1
+ import type {ArraySchemaType} from 'sanity'
2
+ import type {FieldDefinition} from 'sanity'
1
3
  import {Plugin as Plugin_2} from 'sanity'
2
- import {RuleTypeConstraint} from 'sanity'
4
+ import type {Rule} from 'sanity'
5
+ import type {RuleTypeConstraint} from 'sanity'
6
+ import type {SanityClient} from 'sanity'
7
+
8
+ export declare type AllowedType = 'string' | 'number' | 'boolean' | 'text' | 'reference'
9
+
10
+ export declare type ArrayConfig = {
11
+ name: string
12
+ type: AllowedType
13
+ languages: Language[]
14
+ title?: string
15
+ group?: string
16
+ hidden?: boolean | (() => boolean)
17
+ readOnly?: boolean | (() => boolean)
18
+ validation?: Rule | Rule[]
19
+ field?: {
20
+ [key: string]: any
21
+ options: {
22
+ [key: string]: any
23
+ }
24
+ }
25
+ }
26
+
27
+ export declare type ArraySchemaWithLanguageOptions = ArraySchemaType & {
28
+ options: {
29
+ select?: Record<string, string>
30
+ languages: Language[] | LanguageCallback
31
+ apiVersion: string
32
+ }
33
+ }
34
+
35
+ export declare const clear: () => void
3
36
 
4
37
  export declare const internationalizedArray: Plugin_2<PluginConfig>
5
38
 
6
- declare type Language = {
7
- id: string
39
+ export declare type Language = {
40
+ id: Intl.UnicodeBCP47LocaleIdentifier
8
41
  title: string
9
42
  }
10
43
 
11
- declare type PluginConfig = {
12
- languages: Language[] | (() => Promise<Language[]>)
13
- fieldTypes: (string | RuleTypeConstraint)[]
44
+ export declare type LanguageCallback = (
45
+ client: SanityClient,
46
+ selectedValue: Record<string, unknown>
47
+ ) => Promise<Language[]>
48
+
49
+ export declare type PluginConfig = {
50
+ /**
51
+ * https://www.sanity.io/docs/api-versioning
52
+ * @defaultValue '2022-11-27'
53
+ */
54
+ apiVersion?: string
55
+ /**
56
+ * Specify fields that should be available in the language callback:
57
+ * ```tsx
58
+ * {
59
+ * select: {
60
+ * markets: 'markets'
61
+ * },
62
+ * languages: (client, {markets}) =>
63
+ * query.fetch(groq`*[_type == "language" && market in $markets]{id,title}`, {markets})
64
+ * }
65
+ * ```
66
+ */
67
+ select?: Record<string, string>
68
+ /**
69
+ * You can give it an array of language definitions:
70
+ * ```tsx
71
+ * {
72
+ * languages: [
73
+ * {id: 'en', title: 'English'},
74
+ * {id: 'fr', title: 'French'}
75
+ * ]
76
+ * }
77
+ * ```
78
+ * You can load them async by passing a function that returns a promise:
79
+ * ```tsx
80
+ * {
81
+ * languages: async () => {
82
+ * const response = await fetch('https://example.com/languages')
83
+ * return response.json()
84
+ * }
85
+ * }
86
+ * ```
87
+ * You can query your dataset for languages::
88
+ * ```tsx
89
+ * {
90
+ * languages: (client) =>
91
+ * query.fetch(groq`*[_type == "language"]{id,title}`)
92
+ * }
93
+ * ```
94
+ */
95
+ languages: Language[] | LanguageCallback
96
+ /**
97
+ * Can be a string matching core field types, as well as custom ones:
98
+ * ```tsx
99
+ * {
100
+ * fieldTypes: [
101
+ * "date", "datetime", "file", "image", "number", "string", "text", "url"
102
+ * ]
103
+ * }
104
+ * ```
105
+ * You can also define a type directly:
106
+ * ```tsx
107
+ * {
108
+ * fieldTypes: [
109
+ * defineField({
110
+ * name: 'featuredProduct',
111
+ * type: 'reference',
112
+ * to: [{type: 'product'}]
113
+ * hidden: (({document}) => !document?.title)
114
+ * })
115
+ * ]
116
+ * }
117
+ * ```
118
+ */
119
+ fieldTypes: (string | RuleTypeConstraint | FieldDefinition)[]
120
+ }
121
+
122
+ export declare type Value = {
123
+ _key: string
124
+ value?: string
14
125
  }
15
126
 
16
127
  export {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanity-plugin-internationalized-array",
3
- "version": "1.4.1",
3
+ "version": "1.6.0",
4
4
  "description": "Store localized fields in an array to save on attributes",
5
5
  "keywords": [
6
6
  "sanity",
@@ -47,39 +47,43 @@
47
47
  "watch": "pkg-utils watch"
48
48
  },
49
49
  "dependencies": {
50
- "@sanity/icons": "^1.3.1",
50
+ "@sanity/icons": "^2.0.0",
51
51
  "@sanity/incompatible-plugin": "^1.0.4",
52
- "styled-components": "^5.3.5"
52
+ "@sanity/ui": "^1.0.0",
53
+ "fast-deep-equal": "^3.1.3",
54
+ "lodash.get": "^4.4.2",
55
+ "suspend-react": "^0.0.8"
53
56
  },
54
57
  "devDependencies": {
55
- "@commitlint/cli": "^17.2.0",
56
- "@commitlint/config-conventional": "^17.2.0",
58
+ "@commitlint/cli": "^17.3.0",
59
+ "@commitlint/config-conventional": "^17.3.0",
57
60
  "@sanity/pkg-utils": "^1.18.0",
58
- "@sanity/plugin-kit": "^2.1.16",
61
+ "@sanity/plugin-kit": "^2.1.17",
59
62
  "@sanity/semantic-release-preset": "^2.0.2",
60
- "@sanity/ui": "1.0.0-beta.32",
63
+ "@types/lodash.get": "^4.4.7",
61
64
  "@types/styled-components": "^5.1.26",
62
- "@typescript-eslint/eslint-plugin": "^5.43.0",
63
- "@typescript-eslint/parser": "^5.43.0",
64
- "eslint": "^8.27.0",
65
+ "@typescript-eslint/eslint-plugin": "^5.44.0",
66
+ "@typescript-eslint/parser": "^5.44.0",
67
+ "eslint": "^8.28.0",
65
68
  "eslint-config-prettier": "^8.5.0",
66
69
  "eslint-config-sanity": "^6.0.0",
67
70
  "eslint-plugin-prettier": "^4.2.1",
68
- "eslint-plugin-react": "^7.31.10",
71
+ "eslint-plugin-react": "^7.31.11",
69
72
  "eslint-plugin-react-hooks": "^4.6.0",
70
73
  "husky": "^8.0.2",
71
- "lint-staged": "^13.0.3",
72
- "prettier": "^2.7.1",
74
+ "lint-staged": "^13.0.4",
75
+ "prettier": "^2.8.0",
73
76
  "prettier-plugin-packagejson": "^2.3.0",
74
77
  "react": "^18",
75
78
  "rimraf": "^3.0.2",
76
- "sanity": "3.0.0-rc.2",
79
+ "sanity": "3.0.0-rc.3",
80
+ "styled-components": "^5.3.6",
77
81
  "typescript": "^4.9.3"
78
82
  },
79
83
  "peerDependencies": {
80
- "@sanity/ui": "1.0.0-beta.32",
81
84
  "react": "^18",
82
- "sanity": "dev-preview || 3.0.0-rc.2"
85
+ "sanity": "dev-preview || 3.0.0-rc.3 || ^3.0.0",
86
+ "styled-components": "^5.2"
83
87
  },
84
88
  "engines": {
85
89
  "node": ">=14"
package/src/cache.ts ADDED
@@ -0,0 +1,19 @@
1
+ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
2
+
3
+ import * as suspend from 'suspend-react'
4
+ import type {Language} from './types'
5
+
6
+ export const namespace = 'sanity-plugin-internationalized-array'
7
+
8
+ export const version = 'v0'
9
+
10
+ // https://github.com/pmndrs/suspend-react#preloading
11
+ export const preload = (fn: () => Promise<Language[]>) =>
12
+ suspend.preload(() => fn(), [version, namespace])
13
+
14
+ // https://github.com/pmndrs/suspend-react#cache-busting
15
+ export const clear = () => suspend.clear([version, namespace])
16
+
17
+ // https://github.com/pmndrs/suspend-react#peeking-into-entries-outside-of-suspense
18
+ export const peek = (selectedValue: Record<string, unknown>) =>
19
+ suspend.peek([version, namespace, selectedValue]) as Language[] | undefined
@@ -1,4 +1,4 @@
1
- import React, {useCallback, useEffect, useMemo} from 'react'
1
+ import React, {useCallback, useDeferredValue, useEffect, useMemo} from 'react'
2
2
  import {
3
3
  insert,
4
4
  set,
@@ -6,14 +6,20 @@ import {
6
6
  ArrayOfObjectsItemMember,
7
7
  ArrayOfObjectsItem,
8
8
  ArrayOfObjectsInputProps,
9
+ useClient,
10
+ useFormBuilder,
9
11
  } from 'sanity'
10
- import {Button, Grid, Spinner, Stack, useToast} from '@sanity/ui'
12
+ import {Button, Grid, Stack, useToast} from '@sanity/ui'
11
13
  import {AddIcon} from '@sanity/icons'
14
+ import {suspend} from 'suspend-react'
15
+ import equal from 'fast-deep-equal'
12
16
 
13
- import {Language, Value, ArraySchemaWithLanguageOptions} from '../types'
17
+ import type {Value, ArraySchemaWithLanguageOptions} from '../types'
14
18
  import Feedback from './Feedback'
15
19
  // TODO: Move this provider to the root component
16
20
  import {LanguageProvider} from './languageContext'
21
+ import {namespace, version} from '../cache'
22
+ import {getSelectedValue} from './getSelectedValue'
17
23
 
18
24
  export type InternationalizedArrayProps = ArrayOfObjectsInputProps<
19
25
  Value,
@@ -25,22 +31,28 @@ export default function InternationalizedArray(props: InternationalizedArrayProp
25
31
  const readOnly = typeof schemaType.readOnly === 'boolean' ? schemaType.readOnly : false
26
32
  const {options} = schemaType
27
33
  const toast = useToast()
28
-
29
- const [languages, setLanguages] = React.useState<Language[] | null>(
30
- Array.isArray(options.languages) ? options.languages : null
34
+ const {value: document} = useFormBuilder()
35
+ const deferredDocument = useDeferredValue(document)
36
+ const selectedValue = useMemo(
37
+ () => getSelectedValue(options.select, deferredDocument),
38
+ [options.select, deferredDocument]
31
39
  )
32
- // Resolve async languages
33
- useEffect(() => {
34
- async function resolveLanguages() {
35
- const resolvedLanguages = Array.isArray(options.languages)
36
- ? options.languages
37
- : await options.languages()
38
- setLanguages(resolvedLanguages)
39
- }
40
- if (!languages && !Array.isArray(options?.languages)) {
41
- resolveLanguages()
42
- }
43
- }, [languages, options])
40
+
41
+ const {apiVersion} = options
42
+ const client = useClient({apiVersion})
43
+ const languages = Array.isArray(options.languages)
44
+ ? options.languages
45
+ : suspend(
46
+ // eslint-disable-next-line require-await
47
+ async () => {
48
+ if (typeof options.languages === 'function') {
49
+ return options.languages(client, selectedValue)
50
+ }
51
+ return options.languages
52
+ },
53
+ [version, namespace, selectedValue],
54
+ {equal}
55
+ )
44
56
 
45
57
  const handleAddLanguage = useCallback(
46
58
  (languageId?: string) => {
@@ -169,10 +181,6 @@ export default function InternationalizedArray(props: InternationalizedArrayProp
169
181
  return <Feedback />
170
182
  }
171
183
 
172
- if (!languages) {
173
- return <Spinner />
174
- }
175
-
176
184
  return (
177
185
  <LanguageProvider value={{languages}}>
178
186
  <Stack space={2}>
@@ -0,0 +1,18 @@
1
+ import {peek, preload} from '../cache'
2
+ import {memo} from 'react'
3
+ import type {PluginConfig} from '../types'
4
+ import {useClient} from 'sanity'
5
+
6
+ export default memo(function Preload(
7
+ props: Required<Pick<PluginConfig, 'apiVersion' | 'languages'>>
8
+ ) {
9
+ const client = useClient({apiVersion: props.apiVersion})
10
+ if (!Array.isArray(peek({}))) {
11
+ // eslint-disable-next-line require-await
12
+ preload(async () =>
13
+ Array.isArray(props.languages) ? props.languages : props.languages(client, {})
14
+ )
15
+ }
16
+
17
+ return null
18
+ })
@@ -0,0 +1,29 @@
1
+ import {get} from 'lodash'
2
+
3
+ export const getSelectedValue = (
4
+ select: Record<string, string> | undefined,
5
+ document:
6
+ | {
7
+ [x: string]: unknown
8
+ }
9
+ | undefined
10
+ ): Record<string, unknown> => {
11
+ if (!select || !document) {
12
+ return {}
13
+ }
14
+
15
+ const selection: Record<string, string> = select || {}
16
+ const selectedValue: Record<string, unknown> = {}
17
+ for (const [key, path] of Object.entries(selection)) {
18
+ let value = get(document, path)
19
+ if (Array.isArray(value)) {
20
+ // If there are references in the array, ensure they have `_ref` set, otherwise they are considered empty and can safely be ignored
21
+ value = value.filter((item) =>
22
+ typeof item === 'object' ? item?._type === 'reference' && '_ref' in item : true
23
+ )
24
+ }
25
+ selectedValue[key] = value
26
+ }
27
+
28
+ return selectedValue
29
+ }
package/src/index.ts CHANGED
@@ -1 +1,3 @@
1
+ export {clear} from './cache'
1
2
  export {internationalizedArray} from './plugin'
3
+ export * from './types'
package/src/plugin.tsx CHANGED
@@ -1,21 +1,36 @@
1
+ import React from 'react'
1
2
  import {definePlugin} from 'sanity'
2
- import {PluginConfig} from './types'
3
+ import Preload from './components/Preload'
3
4
  import array from './schema/array'
4
5
  import object from './schema/object'
6
+ import {PluginConfig} from './types'
5
7
 
6
- const CONFIG_DEFAULT = {
8
+ const CONFIG_DEFAULT: PluginConfig = {
7
9
  languages: [],
8
10
  fieldTypes: [],
9
11
  }
10
12
 
11
13
  export const internationalizedArray = definePlugin<PluginConfig>((config = CONFIG_DEFAULT) => {
12
- const {languages, fieldTypes} = {...CONFIG_DEFAULT, ...config}
14
+ const {apiVersion = '2022-11-27', select, languages, fieldTypes} = {...CONFIG_DEFAULT, ...config}
13
15
 
14
16
  return {
15
17
  name: 'sanity-plugin-internationalized-array',
18
+ // If `languages` is a callback then let's preload it
19
+ studio: Array.isArray(languages)
20
+ ? undefined
21
+ : {
22
+ components: {
23
+ layout: (props) => (
24
+ <>
25
+ <Preload apiVersion={apiVersion} languages={languages} />
26
+ {props.renderDefault(props)}
27
+ </>
28
+ ),
29
+ },
30
+ },
16
31
  schema: {
17
32
  types: [
18
- ...fieldTypes.map((type) => array({type, languages})),
33
+ ...fieldTypes.map((type) => array({type, apiVersion, select, languages})),
19
34
  ...fieldTypes.map((type) => object({type})),
20
35
  ],
21
36
  },
@@ -1,16 +1,21 @@
1
- import {defineField, FieldDefinition, Rule} from 'sanity'
1
+ /* eslint-disable no-nested-ternary */
2
+ import {defineField, type FieldDefinition, type Rule} from 'sanity'
3
+ import {peek} from '../cache'
2
4
 
3
5
  import {createFieldName} from '../components/createFieldName'
6
+ import {getSelectedValue} from '../components/getSelectedValue'
4
7
  import InternationalizedArray from '../components/InternationalizedArray'
5
- import {Language, Value} from '../types'
8
+ import {Language, LanguageCallback, Value} from '../types'
6
9
 
7
10
  type ArrayFactoryConfig = {
8
- languages: Language[] | (() => Promise<Language[]>)
11
+ apiVersion: string
12
+ select?: Record<string, string>
13
+ languages: Language[] | LanguageCallback
9
14
  type: string | FieldDefinition
10
15
  }
11
16
 
12
17
  export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
13
- const {languages, type} = config
18
+ const {apiVersion, select, languages, type} = config
14
19
  const typeName = typeof type === `string` ? type : type.name
15
20
  const arrayName = createFieldName(typeName)
16
21
  const objectName = createFieldName(typeName, true)
@@ -24,11 +29,12 @@ export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
24
29
  components: {
25
30
  input: InternationalizedArray,
26
31
  },
27
- options: {languages},
32
+ options: {apiVersion, select, languages},
28
33
  // TODO: Resolve this typing issue with the inner object
29
34
  // @ts-ignore
30
35
  of: [
31
36
  defineField({
37
+ ...(typeof type === 'string' ? {} : type),
32
38
  name: objectName,
33
39
  type: objectName,
34
40
  }),
@@ -39,9 +45,13 @@ export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
39
45
  return true
40
46
  }
41
47
 
48
+ const selectedValue = getSelectedValue(select, context.document)
49
+ const client = context.getClient({apiVersion})
42
50
  const contextLanguages: Language[] = Array.isArray(context?.type?.options?.languages)
43
- ? context?.type?.options.languages
44
- : await context?.type?.options.languages()
51
+ ? context!.type!.options.languages
52
+ : Array.isArray(peek(selectedValue))
53
+ ? peek(selectedValue)
54
+ : await context?.type?.options.languages(client, selectedValue)
45
55
 
46
56
  if (value && value.length > contextLanguages.length) {
47
57
  return `Cannot be more than ${
package/src/types.ts CHANGED
@@ -1,7 +1,7 @@
1
- import {Rule, ArraySchemaType, RuleTypeConstraint} from 'sanity'
1
+ import type {Rule, ArraySchemaType, RuleTypeConstraint, SanityClient, FieldDefinition} from 'sanity'
2
2
 
3
3
  export type Language = {
4
- id: string
4
+ id: Intl.UnicodeBCP47LocaleIdentifier
5
5
  title: string
6
6
  }
7
7
 
@@ -24,14 +24,88 @@ export type Value = {
24
24
  value?: string
25
25
  }
26
26
 
27
+ export type LanguageCallback = (
28
+ client: SanityClient,
29
+ selectedValue: Record<string, unknown>
30
+ ) => Promise<Language[]>
31
+
27
32
  export type PluginConfig = {
28
- // array of languages or async function that returns array of languages
29
- languages: Language[] | (() => Promise<Language[]>)
30
- fieldTypes: (string | RuleTypeConstraint)[]
33
+ /**
34
+ * https://www.sanity.io/docs/api-versioning
35
+ * @defaultValue '2022-11-27'
36
+ */
37
+ apiVersion?: string
38
+ /**
39
+ * Specify fields that should be available in the language callback:
40
+ * ```tsx
41
+ * {
42
+ * select: {
43
+ * markets: 'markets'
44
+ * },
45
+ * languages: (client, {markets}) =>
46
+ * query.fetch(groq`*[_type == "language" && market in $markets]{id,title}`, {markets})
47
+ * }
48
+ * ```
49
+ */
50
+ select?: Record<string, string>
51
+ /**
52
+ * You can give it an array of language definitions:
53
+ * ```tsx
54
+ * {
55
+ * languages: [
56
+ * {id: 'en', title: 'English'},
57
+ * {id: 'fr', title: 'French'}
58
+ * ]
59
+ * }
60
+ * ```
61
+ * You can load them async by passing a function that returns a promise:
62
+ * ```tsx
63
+ * {
64
+ * languages: async () => {
65
+ * const response = await fetch('https://example.com/languages')
66
+ * return response.json()
67
+ * }
68
+ * }
69
+ * ```
70
+ * You can query your dataset for languages::
71
+ * ```tsx
72
+ * {
73
+ * languages: (client) =>
74
+ * query.fetch(groq`*[_type == "language"]{id,title}`)
75
+ * }
76
+ * ```
77
+ */
78
+ languages: Language[] | LanguageCallback
79
+ /**
80
+ * Can be a string matching core field types, as well as custom ones:
81
+ * ```tsx
82
+ * {
83
+ * fieldTypes: [
84
+ * "date", "datetime", "file", "image", "number", "string", "text", "url"
85
+ * ]
86
+ * }
87
+ * ```
88
+ * You can also define a type directly:
89
+ * ```tsx
90
+ * {
91
+ * fieldTypes: [
92
+ * defineField({
93
+ * name: 'featuredProduct',
94
+ * type: 'reference',
95
+ * to: [{type: 'product'}]
96
+ * hidden: (({document}) => !document?.title)
97
+ * })
98
+ * ]
99
+ * }
100
+ * ```
101
+ */
102
+ fieldTypes: (string | RuleTypeConstraint | FieldDefinition)[]
31
103
  }
32
104
 
33
105
  export type ArraySchemaWithLanguageOptions = ArraySchemaType & {
34
106
  options: {
35
- languages: Language[] | (() => Promise<Language[]>)
107
+ select?: Record<string, string>
108
+ languages: Language[] | LanguageCallback
109
+ apiVersion: string
36
110
  }
37
111
  }