sanity-plugin-internationalized-array 1.5.0 → 1.6.1

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.5.0",
3
+ "version": "1.6.1",
4
4
  "description": "Store localized fields in an array to save on attributes",
5
5
  "keywords": [
6
6
  "sanity",
@@ -15,13 +15,13 @@
15
15
  "url": "git@github.com:SimeonGriggs/sanity-plugin-internationalized-array.git"
16
16
  },
17
17
  "license": "MIT",
18
- "author": "Simeon Griggs <simeon@sanity.io>",
18
+ "author": "Sanity.io <hello@sanity.io>",
19
19
  "exports": {
20
20
  ".": {
21
- "types": "./lib/src/index.d.ts",
21
+ "types": "./lib/index.d.ts",
22
22
  "source": "./src/index.ts",
23
- "import": "./lib/index.esm.js",
24
23
  "require": "./lib/index.js",
24
+ "import": "./lib/index.esm.js",
25
25
  "default": "./lib/index.esm.js"
26
26
  },
27
27
  "./package.json": "./package.json"
@@ -29,60 +29,75 @@
29
29
  "main": "./lib/index.js",
30
30
  "module": "./lib/index.esm.js",
31
31
  "source": "./src/index.ts",
32
- "types": "./lib/src/index.d.ts",
32
+ "types": "./lib/index.d.ts",
33
33
  "files": [
34
- "src",
35
34
  "lib",
36
- "v2-incompatible.js",
37
- "sanity.json"
35
+ "sanity.json",
36
+ "src",
37
+ "v2-incompatible.js"
38
38
  ],
39
39
  "scripts": {
40
40
  "prebuild": "npm run clean && plugin-kit verify-package --silent && pkg-utils",
41
- "build": "pkg-utils build",
41
+ "build": "run-s clean && plugin-kit verify-package --silent && pkg-utils build --strict && pkg-utils --strict",
42
42
  "clean": "rimraf lib",
43
+ "format": "prettier --write --cache --ignore-unknown .",
43
44
  "link-watch": "plugin-kit link-watch",
44
45
  "lint": "eslint .",
46
+ "lint:fix": "eslint . --fix",
45
47
  "prepare": "husky install",
46
- "prepublishOnly": "npm run build",
47
- "watch": "pkg-utils watch"
48
+ "prepublishOnly": "run-s build",
49
+ "watch": "pkg-utils watch --strict"
48
50
  },
49
51
  "dependencies": {
50
- "@sanity/icons": "^2.0.0",
52
+ "@sanity/icons": "^2.2.2",
51
53
  "@sanity/incompatible-plugin": "^1.0.4",
52
- "@sanity/ui": "^1.0.0",
54
+ "@sanity/ui": "^1.2.2",
55
+ "fast-deep-equal": "^3.1.3",
56
+ "lodash.get": "^4.4.2",
53
57
  "suspend-react": "^0.0.8"
54
58
  },
55
59
  "devDependencies": {
56
- "@commitlint/cli": "^17.3.0",
57
- "@commitlint/config-conventional": "^17.3.0",
58
- "@sanity/pkg-utils": "^1.18.0",
59
- "@sanity/plugin-kit": "^2.1.17",
60
- "@sanity/semantic-release-preset": "^2.0.2",
60
+ "@commitlint/cli": "^17.4.4",
61
+ "@commitlint/config-conventional": "^17.4.4",
62
+ "@sanity/pkg-utils": "^2.2.4",
63
+ "@sanity/plugin-kit": "^3.1.4",
64
+ "@sanity/semantic-release-preset": "^4.0.1",
65
+ "@types/react": "^18.0.27",
61
66
  "@types/styled-components": "^5.1.26",
62
- "@typescript-eslint/eslint-plugin": "^5.44.0",
63
- "@typescript-eslint/parser": "^5.44.0",
64
- "eslint": "^8.28.0",
65
- "eslint-config-prettier": "^8.5.0",
67
+ "@typescript-eslint/eslint-plugin": "^5.51.0",
68
+ "@typescript-eslint/parser": "^5.51.0",
69
+ "eslint": "^8.33.0",
70
+ "eslint-config-prettier": "^8.6.0",
66
71
  "eslint-config-sanity": "^6.0.0",
67
72
  "eslint-plugin-prettier": "^4.2.1",
68
- "eslint-plugin-react": "^7.31.11",
73
+ "eslint-plugin-react": "^7.32.2",
69
74
  "eslint-plugin-react-hooks": "^4.6.0",
70
- "husky": "^8.0.2",
71
- "lint-staged": "^13.0.4",
72
- "prettier": "^2.8.0",
73
- "prettier-plugin-packagejson": "^2.3.0",
75
+ "eslint-plugin-simple-import-sort": "^10.0.0",
76
+ "husky": "^8.0.3",
77
+ "lint-staged": "^13.2.0",
78
+ "npm-run-all": "^4.1.5",
79
+ "prettier": "^2.8.4",
80
+ "prettier-plugin-packagejson": "^2.4.2",
74
81
  "react": "^18",
75
- "rimraf": "^3.0.2",
76
- "sanity": "3.0.0-rc.3",
77
- "styled-components": "^5.3.6",
78
- "typescript": "^4.9.3"
82
+ "react-dom": "^18",
83
+ "react-is": "^18",
84
+ "rimraf": "^4.1.2",
85
+ "sanity": "^3.3.1",
86
+ "semantic-release": "^20.1.0",
87
+ "typescript": "^4.9.5"
79
88
  },
80
89
  "peerDependencies": {
90
+ "@sanity/ui": "^1.2.2",
81
91
  "react": "^18",
82
- "sanity": "dev-preview || 3.0.0-rc.3 || ^3.0.0",
83
- "styled-components": "^5.2"
92
+ "sanity": "^3.0.0",
93
+ "styled-components": "^5.3.6"
84
94
  },
85
95
  "engines": {
86
96
  "node": ">=14"
97
+ },
98
+ "sanityPlugin": {
99
+ "verifyPackage": {
100
+ "eslintImports": false
101
+ }
87
102
  }
88
103
  }
package/src/cache.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
2
2
 
3
3
  import * as suspend from 'suspend-react'
4
+
4
5
  import type {Language} from './types'
5
6
 
6
7
  export const namespace = 'sanity-plugin-internationalized-array'
@@ -15,4 +16,5 @@ export const preload = (fn: () => Promise<Language[]>) =>
15
16
  export const clear = () => suspend.clear([version, namespace])
16
17
 
17
18
  // https://github.com/pmndrs/suspend-react#peeking-into-entries-outside-of-suspense
18
- export const peek = () => suspend.peek([version, namespace]) as Language[] | undefined
19
+ export const peek = (selectedValue: Record<string, unknown>) =>
20
+ suspend.peek([version, namespace, selectedValue]) as Language[] | undefined
@@ -1,45 +1,61 @@
1
- import React, {Fragment, useCallback, useEffect, useMemo} from 'react'
1
+ import {AddIcon} from '@sanity/icons'
2
+ import {Button, Grid, Stack, useToast} from '@sanity/ui'
3
+ import equal from 'fast-deep-equal'
4
+ import {useCallback, useDeferredValue, useEffect, useMemo} from 'react'
2
5
  import {
6
+ ArrayOfObjectsInputProps,
7
+ ArrayOfObjectsItem,
8
+ ArrayOfObjectsItemMember,
3
9
  insert,
4
10
  set,
5
11
  setIfMissing,
6
- ArrayOfObjectsItemMember,
7
- ArrayOfObjectsItem,
8
- ArrayOfObjectsInputProps,
9
12
  useClient,
13
+ useFormBuilder,
10
14
  } from 'sanity'
11
- import {Button, Grid, Stack, useToast} from '@sanity/ui'
12
- import {AddIcon} from '@sanity/icons'
13
15
  import {suspend} from 'suspend-react'
14
16
 
15
- import type {Value, ArraySchemaWithLanguageOptions} from '../types'
17
+ import {namespace, version} from '../cache'
18
+ import type {ArraySchemaWithLanguageOptions, Value} from '../types'
16
19
  import Feedback from './Feedback'
20
+ import {getSelectedValue} from './getSelectedValue'
17
21
  // TODO: Move this provider to the root component
18
22
  import {LanguageProvider} from './languageContext'
19
- import {namespace, version} from '../cache'
20
23
 
21
24
  export type InternationalizedArrayProps = ArrayOfObjectsInputProps<
22
25
  Value,
23
26
  ArraySchemaWithLanguageOptions
24
27
  >
25
28
 
26
- export default function InternationalizedArray(props: InternationalizedArrayProps) {
29
+ export default function InternationalizedArray(
30
+ props: InternationalizedArrayProps
31
+ ) {
27
32
  const {members, value, schemaType, onChange} = props
28
- const readOnly = typeof schemaType.readOnly === 'boolean' ? schemaType.readOnly : false
33
+ const readOnly =
34
+ typeof schemaType.readOnly === 'boolean' ? schemaType.readOnly : false
29
35
  const {options} = schemaType
30
36
  const toast = useToast()
37
+ const {value: document} = useFormBuilder()
38
+ const deferredDocument = useDeferredValue(document)
39
+ const selectedValue = useMemo(
40
+ () => getSelectedValue(options.select, deferredDocument),
41
+ [options.select, deferredDocument]
42
+ )
31
43
 
32
44
  const {apiVersion} = options
33
45
  const client = useClient({apiVersion})
34
46
  const languages = Array.isArray(options.languages)
35
47
  ? options.languages
36
- : // eslint-disable-next-line require-await
37
- suspend(async () => {
38
- if (typeof options.languages === 'function') {
39
- return options.languages(client)
40
- }
41
- return options.languages
42
- }, [version, namespace])
48
+ : suspend(
49
+ // eslint-disable-next-line require-await
50
+ async () => {
51
+ if (typeof options.languages === 'function') {
52
+ return options.languages(client, selectedValue)
53
+ }
54
+ return options.languages
55
+ },
56
+ [version, namespace, selectedValue],
57
+ {equal}
58
+ )
43
59
 
44
60
  const handleAddLanguage = useCallback(
45
61
  (languageId?: string) => {
@@ -148,13 +164,16 @@ export default function InternationalizedArray(props: InternationalizedArrayProp
148
164
  }
149
165
 
150
166
  return value
151
- .map((v, vIndex) => (vIndex === languagesInUse.findIndex((l) => l.id === v._key) ? null : v))
167
+ .map((v, vIndex) =>
168
+ vIndex === languagesInUse.findIndex((l) => l.id === v._key) ? null : v
169
+ )
152
170
  .filter(Boolean)
153
171
  }, [value, languagesInUse])
154
172
 
155
173
  const languagesAreValid = useMemo(
156
174
  () =>
157
- !languages?.length || (languages?.length && languages.every((item) => item.id && item.title)),
175
+ !languages?.length ||
176
+ (languages?.length && languages.every((item) => item.id && item.title)),
158
177
  [languages]
159
178
  )
160
179
 
@@ -218,7 +237,10 @@ export default function InternationalizedArray(props: InternationalizedArrayProp
218
237
  tone="primary"
219
238
  mode="ghost"
220
239
  fontSize={1}
221
- disabled={readOnly || Boolean(value?.find((item) => item._key === language.id))}
240
+ disabled={
241
+ readOnly ||
242
+ Boolean(value?.find((item) => item._key === language.id))
243
+ }
222
244
  text={language.id.toUpperCase()}
223
245
  icon={AddIcon}
224
246
  onClick={() => handleAddLanguage(language.id)}
@@ -229,13 +251,17 @@ export default function InternationalizedArray(props: InternationalizedArrayProp
229
251
  <Button
230
252
  tone="primary"
231
253
  mode="ghost"
232
- disabled={readOnly || (value && value?.length >= languages?.length)}
254
+ disabled={
255
+ readOnly || (value && value?.length >= languages?.length)
256
+ }
233
257
  icon={AddIcon}
234
258
  text={
235
259
  // eslint-disable-next-line no-nested-ternary
236
260
  value?.length
237
261
  ? `Add missing ${
238
- languages.length - value.length === 1 ? `language` : `languages`
262
+ languages.length - value.length === 1
263
+ ? `language`
264
+ : `languages`
239
265
  }`
240
266
  : languages.length === 1
241
267
  ? `Add ${languages[0].title} Field`
@@ -7,10 +7,10 @@ export default memo(function Preload(
7
7
  props: Required<Pick<PluginConfig, 'apiVersion' | 'languages'>>
8
8
  ) {
9
9
  const client = useClient({apiVersion: props.apiVersion})
10
- if (!Array.isArray(peek())) {
10
+ if (!Array.isArray(peek({}))) {
11
11
  // eslint-disable-next-line require-await
12
12
  preload(async () =>
13
- Array.isArray(props.languages) ? props.languages : props.languages(client)
13
+ Array.isArray(props.languages) ? props.languages : props.languages(client, {})
14
14
  )
15
15
  }
16
16
 
@@ -1,6 +1,6 @@
1
+ import {Box, BoxProps, Card, CardProps} from '@sanity/ui'
1
2
  import React from 'react'
2
3
  import styled, {css} from 'styled-components'
3
- import {Box, BoxProps, Card, CardProps} from '@sanity/ui'
4
4
 
5
5
  // Wrappers required because of bug with passing down "as" prop
6
6
  // https://github.com/styled-components/styled-components/issues/2449
@@ -0,0 +1,31 @@
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'
23
+ ? item?._type === 'reference' && '_ref' in item
24
+ : true
25
+ )
26
+ }
27
+ selectedValue[key] = value
28
+ }
29
+
30
+ return selectedValue
31
+ }
@@ -1,7 +1,9 @@
1
1
  import {CardTone} from '@sanity/ui'
2
2
  import {FormNodeValidation} from 'sanity'
3
3
 
4
- export function getToneFromValidation(validations: FormNodeValidation[]): CardTone | undefined {
4
+ export function getToneFromValidation(
5
+ validations: FormNodeValidation[]
6
+ ): CardTone | undefined {
5
7
  if (!validations?.length) {
6
8
  return undefined
7
9
  }
package/src/plugin.tsx CHANGED
@@ -11,7 +11,7 @@ const CONFIG_DEFAULT: PluginConfig = {
11
11
  }
12
12
 
13
13
  export const internationalizedArray = definePlugin<PluginConfig>((config = CONFIG_DEFAULT) => {
14
- const {apiVersion = '2022-11-27', languages, fieldTypes} = {...CONFIG_DEFAULT, ...config}
14
+ const {apiVersion = '2022-11-27', select, languages, fieldTypes} = {...CONFIG_DEFAULT, ...config}
15
15
 
16
16
  return {
17
17
  name: 'sanity-plugin-internationalized-array',
@@ -30,7 +30,7 @@ export const internationalizedArray = definePlugin<PluginConfig>((config = CONFI
30
30
  },
31
31
  schema: {
32
32
  types: [
33
- ...fieldTypes.map((type) => array({type, apiVersion, languages})),
33
+ ...fieldTypes.map((type) => array({type, apiVersion, select, languages})),
34
34
  ...fieldTypes.map((type) => object({type})),
35
35
  ],
36
36
  },
@@ -1,19 +1,21 @@
1
1
  /* eslint-disable no-nested-ternary */
2
- import {defineField, type FieldDefinition, type Rule, type SanityClient} from 'sanity'
3
- import {peek} from '../cache'
2
+ import {defineField, type FieldDefinition, type Rule} from 'sanity'
4
3
 
4
+ import {peek} from '../cache'
5
5
  import {createFieldName} from '../components/createFieldName'
6
+ import {getSelectedValue} from '../components/getSelectedValue'
6
7
  import InternationalizedArray from '../components/InternationalizedArray'
7
- import {Language, Value} from '../types'
8
+ import {Language, LanguageCallback, Value} from '../types'
8
9
 
9
10
  type ArrayFactoryConfig = {
10
11
  apiVersion: string
11
- languages: Language[] | ((client: SanityClient) => Promise<Language[]>)
12
+ select?: Record<string, string>
13
+ languages: Language[] | LanguageCallback
12
14
  type: string | FieldDefinition
13
15
  }
14
16
 
15
17
  export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
16
- const {apiVersion, languages, type} = config
18
+ const {apiVersion, select, languages, type} = config
17
19
  const typeName = typeof type === `string` ? type : type.name
18
20
  const arrayName = createFieldName(typeName)
19
21
  const objectName = createFieldName(typeName, true)
@@ -22,14 +24,12 @@ export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
22
24
  name: arrayName,
23
25
  title: 'Internationalized array',
24
26
  type: 'array',
25
- // TODO: Resolve this typing issue with the outer component
26
- // @ts-ignore
27
27
  components: {
28
28
  input: InternationalizedArray,
29
29
  },
30
- options: {apiVersion, languages},
30
+ options: {apiVersion, select, languages},
31
31
  // TODO: Resolve this typing issue with the inner object
32
- // @ts-ignore
32
+ // @ts-expect-error
33
33
  of: [
34
34
  defineField({
35
35
  ...(typeof type === 'string' ? {} : type),
@@ -43,21 +43,29 @@ export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
43
43
  return true
44
44
  }
45
45
 
46
+ const selectedValue = getSelectedValue(select, context.document)
46
47
  const client = context.getClient({apiVersion})
47
- const contextLanguages: Language[] = Array.isArray(context?.type?.options?.languages)
48
+ const contextLanguages: Language[] = Array.isArray(
49
+ context?.type?.options?.languages
50
+ )
48
51
  ? context!.type!.options.languages
49
- : Array.isArray(peek())
50
- ? peek()
51
- : await context?.type?.options.languages(client)
52
+ : Array.isArray(peek(selectedValue))
53
+ ? peek(selectedValue)
54
+ : await context?.type?.options.languages(client, selectedValue)
52
55
 
53
56
  if (value && value.length > contextLanguages.length) {
54
57
  return `Cannot be more than ${
55
- contextLanguages.length === 1 ? `1 item` : `${contextLanguages.length} items`
58
+ contextLanguages.length === 1
59
+ ? `1 item`
60
+ : `${contextLanguages.length} items`
56
61
  }`
57
62
  }
58
63
 
59
64
  const nonLanguageKeys = value?.length
60
- ? value.filter((item) => !contextLanguages.find((language) => item._key === language.id))
65
+ ? value.filter(
66
+ (item) =>
67
+ !contextLanguages.find((language) => item._key === language.id)
68
+ )
61
69
  : []
62
70
  if (nonLanguageKeys.length) {
63
71
  return {
@@ -17,26 +17,20 @@ export default (config: ObjectFactoryConfig): FieldDefinition<'object'> => {
17
17
  name: objectName,
18
18
  title: `Internationalized array ${type}`,
19
19
  type: 'object',
20
- // TODO: Resolve this typing issue with the return type
21
- // @ts-ignore
22
20
  components: {
23
- // TODO: Resolve this typing issue with the outer component
24
- // @ts-ignore
25
21
  item: InternationalizedInput,
26
22
  },
27
23
  // TODO: Address this typing issue with the inner object
28
- // @ts-ignore
24
+ // @ts-expect-error
29
25
  fields: [
30
26
  typeof type === `string`
31
27
  ? // Define a simple field if all we have is the name as a string
32
28
  defineField({
33
29
  name: 'value',
34
30
  type,
35
- // TODO: Address this typing issue with components on a dynamic `type`
36
- // @ts-ignore
37
31
  components: {
38
32
  // TODO: Address this typing issue with the inner object
39
- // @ts-ignore
33
+ // @ts-expect-error
40
34
  field: InternationalizedField,
41
35
  },
42
36
  })
package/src/types.ts CHANGED
@@ -1,4 +1,10 @@
1
- import type {Rule, ArraySchemaType, RuleTypeConstraint, SanityClient} from 'sanity'
1
+ import type {
2
+ ArraySchemaType,
3
+ FieldDefinition,
4
+ Rule,
5
+ RuleTypeConstraint,
6
+ SanityClient,
7
+ } from 'sanity'
2
8
 
3
9
  export type Language = {
4
10
  id: Intl.UnicodeBCP47LocaleIdentifier
@@ -24,12 +30,30 @@ export type Value = {
24
30
  value?: string
25
31
  }
26
32
 
33
+ export type LanguageCallback = (
34
+ client: SanityClient,
35
+ selectedValue: Record<string, unknown>
36
+ ) => Promise<Language[]>
37
+
27
38
  export type PluginConfig = {
28
39
  /**
29
40
  * https://www.sanity.io/docs/api-versioning
30
41
  * @defaultValue '2022-11-27'
31
42
  */
32
43
  apiVersion?: string
44
+ /**
45
+ * Specify fields that should be available in the language callback:
46
+ * ```tsx
47
+ * {
48
+ * select: {
49
+ * markets: 'markets'
50
+ * },
51
+ * languages: (client, {markets}) =>
52
+ * query.fetch(groq`*[_type == "language" && market in $markets]{id,title}`, {markets})
53
+ * }
54
+ * ```
55
+ */
56
+ select?: Record<string, string>
33
57
  /**
34
58
  * You can give it an array of language definitions:
35
59
  * ```tsx
@@ -57,7 +81,7 @@ export type PluginConfig = {
57
81
  * }
58
82
  * ```
59
83
  */
60
- languages: Language[] | ((client: SanityClient) => Promise<Language[]>)
84
+ languages: Language[] | LanguageCallback
61
85
  /**
62
86
  * Can be a string matching core field types, as well as custom ones:
63
87
  * ```tsx
@@ -81,12 +105,13 @@ export type PluginConfig = {
81
105
  * }
82
106
  * ```
83
107
  */
84
- fieldTypes: (string | RuleTypeConstraint)[]
108
+ fieldTypes: (string | RuleTypeConstraint | FieldDefinition)[]
85
109
  }
86
110
 
87
111
  export type ArraySchemaWithLanguageOptions = ArraySchemaType & {
88
112
  options: {
89
- languages: Language[] | ((client: SanityClient) => Promise<Language[]>)
113
+ select?: Record<string, string>
114
+ languages: Language[] | LanguageCallback
90
115
  apiVersion: string
91
116
  }
92
117
  }