sanity-plugin-internationalized-array 1.6.2 → 1.8.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/README.md CHANGED
@@ -36,6 +36,7 @@ import {internationalizedArray} from 'sanity-plugin-internationalized-array'
36
36
  {id: 'en', title: 'English'},
37
37
  {id: 'fr', title: 'French'}
38
38
  ],
39
+ defaultLanguages: ['en'],
39
40
  fieldTypes: ['string'],
40
41
  })
41
42
  ]
@@ -47,6 +48,8 @@ This will register two new fields to the schema, based on the settings passed in
47
48
  - `internationalizedArrayString` an array field of:
48
49
  - `internationalizedArrayStringValue` an object field, with a single `string` field inside called `value`
49
50
 
51
+ The above config will also create an empty array item in new documents for each language in `defaultLanguages`. This is configured globally for all internationalized array fields.
52
+
50
53
  You can pass in more registered schema-type names to generate more internationalized arrays. Use them in your schema like this:
51
54
 
52
55
  ```ts
@@ -102,7 +105,29 @@ languages: async (client, {market = ``}) => {
102
105
  },
103
106
  ```
104
107
 
105
- ## Using more complex field types
108
+ ## Configuring the "Add translation" buttons
109
+
110
+ The "Add translation" buttons can be positioned below the field, inside Field Actions (⚠️ currently unstable ⚠️) or both with `buttonLocations`.
111
+
112
+ The "Add all languages" button can be hidden with `buttonAddAll`.
113
+
114
+ ```ts
115
+ import {defineConfig} from 'sanity'
116
+ import {internationalizedArray} from 'sanity-plugin-internationalized-array'
117
+
118
+ export const defineConfig({
119
+ // ...
120
+ plugins: [
121
+ internationalizedArray({
122
+ // ...other config
123
+ buttonLocations: ['field', 'unstable__fieldAction'] // default ['field']
124
+ buttonAddAll: false // default true
125
+ })
126
+ ]
127
+ })
128
+ ```
129
+
130
+ ## Using complex field configurations
106
131
 
107
132
  For more control over the `value` field, you can pass a schema definition into the `fieldTypes` array.
108
133
 
@@ -138,6 +163,109 @@ This would also create two new fields in your schema.
138
163
 
139
164
  Note that the `name` key in the field gets rewritten to `value` and is instead used to name the object field.
140
165
 
166
+ ## Creating internationalized objects
167
+
168
+ Due to how fields are created, you cannot use anonymous objects in the `fieldTypes` array. You must register the object type in your Studio's schema as an "alias type".
169
+
170
+ ```ts
171
+ // ./schemas/seoType.ts
172
+
173
+ import {defineField} from 'sanity'
174
+
175
+ export const seoType = defineField({
176
+ name: 'seo',
177
+ title: 'SEO',
178
+ type: 'object',
179
+ fields: [
180
+ defineField({name: 'title', type: 'string'}),
181
+ defineField({name: 'description', type: 'string'}),
182
+ ],
183
+ })
184
+ ```
185
+
186
+ Then in your plugin configuration settings, add the name of your alias type to the `fieldTypes` setting.
187
+
188
+ ```ts
189
+ internationalizedArray({
190
+ languages: [
191
+ //...languages
192
+ ],
193
+ fieldTypes: ['seo'],
194
+ })
195
+ ```
196
+
197
+ Lastly, add the field to your schema.
198
+
199
+ ```ts
200
+ // ./schemas/post.ts
201
+
202
+ import {defineField, defineType} from 'sanity'
203
+
204
+ export default defineType({
205
+ name: 'post',
206
+ title: 'Post',
207
+ type: 'document',
208
+ fields: [
209
+ defineField({
210
+ name: 'seo',
211
+ type: 'internationalizedArraySeo',
212
+ }),
213
+ ],
214
+ })
215
+ ```
216
+
217
+ ## Usage with @sanity/language-filter
218
+
219
+ If you have many languages and authors that predominately write in only a few, [@sanity/language-filter](https://github.com/sanity-io/language-filter) can be used to reduce the number of language fields shown in the document form.
220
+
221
+ ![Internationalized array field filtered with language-filter](https://github.com/sanity-io/language-filter/assets/9684022/4b402520-4128-4e6e-af07-960a10be397e)
222
+
223
+ Configure both plugins in your sanity.config.ts file:
224
+
225
+ ```ts
226
+ // ./sanity.config.ts
227
+
228
+ import {languageFilter} from '@sanity/language-filter'
229
+
230
+ export default defineConfig({
231
+ // ... other config
232
+ plugins: [
233
+ // ... other plugins
234
+ languageFilter({
235
+ // Use the same languages as the internationalized array plugin
236
+ supportedLanguages: SUPPORTED_LANGUAGES,
237
+ defaultLanguages: [],
238
+ documentTypes: ['post'],
239
+ filterField: (enclosingType, member, selectedLanguageIds) => {
240
+ // Filter internationalized arrays
241
+ if (
242
+ enclosingType.jsonType === 'object' &&
243
+ enclosingType.name.startsWith('internationalizedArray') &&
244
+ 'kind' in member
245
+ ) {
246
+ const language = isKeyedObject(member.field.path[1])
247
+ ? member.field.path[1]._key
248
+ : null
249
+
250
+ return language ? selectedLanguageIds.includes(language) : false
251
+ }
252
+
253
+ // Filter internationalized objects if you have them
254
+ // `localeString` must be registered as a custom schema type
255
+ if (
256
+ enclosingType.jsonType === 'object' &&
257
+ enclosingType.name.startsWith('locale')
258
+ ) {
259
+ return selectedLanguageIds.includes(member.name)
260
+ }
261
+
262
+ return true
263
+ },
264
+ }),
265
+ ],
266
+ })
267
+ ```
268
+
141
269
  ## Shape of stored data
142
270
 
143
271
  The custom input contains buttons which will add new array items with the language as the `_key` value. Data returned from this array will look like this:
@@ -161,7 +289,7 @@ Using GROQ filters you can query for a specific language key like so:
161
289
 
162
290
  ## Migrate from objects to arrays
163
291
 
164
- [See the migration script](https://github.com/sanity-io/sanity-plugin-internationalized-array/blob/main/migrations/transformObjectToArray.js) inside `./migrations/transformObjectToArray.js` of this Repo.
292
+ [See the migration script](https://github.com/sanity-io/sanity-plugin-internationalized-array/blob/main/migrations/transformObjectToArray.ts) inside `./migrations/transformObjectToArray.ts` of this Repo.
165
293
 
166
294
  Follow the instructions inside the script and set the `_type` and field name you wish to target.
167
295
 
@@ -169,11 +297,11 @@ Please take a backup first!
169
297
 
170
298
  ### Why store localized field data like this?
171
299
 
172
- The most popular way to store field-level translated content is in an object using the method prescribed in [@sanity/language-filter](https://www.npmjs.com/package/@sanity/language-filter). This works well and creates tidy object structures, but also create a unique field path for every unique field name, multiplied by the number of languages in your dataset.
300
+ The most popular way to store field-level translated content is in an object using the method prescribed in [@sanity/language-filter](https://www.npmjs.com/package/@sanity/language-filter). This works well and creates tidy object structures, but also creates a unique field path for every unique field name, multiplied by the number of languages in your dataset.
173
301
 
174
- For most people, this won't become an issue. On a very large dataset with a lot of languages, the [Attribute Limit](https://www.sanity.io/docs/attribute-limit) can become a concern. This plugin's arrays will use less attributes than an object once you have more than three languages.
302
+ For most people, this won't become an issue. On a very large dataset with a lot of languages, the [Attribute Limit](https://www.sanity.io/docs/attribute-limit) can become a concern. This plugin's arrays will use fewer attributes than an object once you have more than three languages.
175
303
 
176
- The same content as above, plus a third language, structed as an `object` of `string` fields looks like this:
304
+ The same content as above, plus a third language, structured as an `object` of `string` fields looks like this:
177
305
 
178
306
  ```json
179
307
  "greeting" {
@@ -194,7 +322,7 @@ greeting.es
194
322
 
195
323
  Every language you add to every object that uses this structure will add to the number of unique query paths.
196
324
 
197
- The array created by this plugin creates four query paths by default, but is not effected by the number of languages:
325
+ The array created by this plugin creates four query paths by default, but is not affected by the number of languages:
198
326
 
199
327
  ```
200
328
  greeting
@@ -205,7 +333,7 @@ greeting[].value
205
333
 
206
334
  By using this plugin you can safely extend the number of languages without adding any additional query paths.
207
335
 
208
- MIT © Simeon Griggs
336
+ MIT © Sanity.io
209
337
  See LICENSE
210
338
 
211
339
  ## License
package/lib/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- import type {ArraySchemaType} from 'sanity'
2
1
  import type {FieldDefinition} from 'sanity'
3
2
  import {Plugin as Plugin_2} from 'sanity'
4
3
  import type {Rule} from 'sanity'
@@ -29,14 +28,6 @@ export declare type ArrayConfig = {
29
28
  }
30
29
  }
31
30
 
32
- export declare type ArraySchemaWithLanguageOptions = ArraySchemaType & {
33
- options: {
34
- select?: Record<string, string>
35
- languages: Language[] | LanguageCallback
36
- apiVersion: string
37
- }
38
- }
39
-
40
31
  export declare const clear: () => void
41
32
 
42
33
  export declare const internationalizedArray: Plugin_2<PluginConfig>
@@ -98,6 +89,15 @@ export declare type PluginConfig = {
98
89
  * ```
99
90
  */
100
91
  languages: Language[] | LanguageCallback
92
+ /**
93
+ * You can specify a list of language IDs that should be pre-filled when creating a new document
94
+ * ```tsx
95
+ * {
96
+ * defaultLanguages: ['en']
97
+ * }
98
+ * ```
99
+ */
100
+ defaultLanguages?: string[]
101
101
  /**
102
102
  * Can be a string matching core field types, as well as custom ones:
103
103
  * ```tsx
@@ -122,6 +122,16 @@ export declare type PluginConfig = {
122
122
  * ```
123
123
  */
124
124
  fieldTypes: (string | RuleTypeConstraint | FieldDefinition)[]
125
+ /**
126
+ * Locations where the "+ EN" add language buttons are visible
127
+ * @defaultValue ['field']
128
+ * */
129
+ buttonLocations: ('field' | 'unstable__fieldAction')[]
130
+ /**
131
+ * Show or hide the "Add missing languages" button
132
+ * @defaultValue true
133
+ * */
134
+ buttonAddAll: boolean
125
135
  }
126
136
 
127
137
  export declare type Value = {
package/lib/index.esm.js CHANGED
@@ -1,4 +1,4 @@
1
- import*as suspend from'suspend-react';import{suspend as suspend$1}from'suspend-react';import{jsx,jsxs,Fragment}from'react/jsx-runtime';import{useClient,useFormBuilder,insert,setIfMissing,set,ArrayOfObjectsItem,defineField,useFormValue,unset,definePlugin}from'sanity';import React,{memo,useDeferredValue,useMemo,useCallback,useEffect}from'react';import{AddIcon,RemoveCircleIcon}from'@sanity/icons';import{Card,Stack,Text,Code,useToast,Grid,Button,Spinner,Label,MenuButton,Menu,MenuItem,Flex}from'@sanity/ui';import equal from'fast-deep-equal';const namespace="sanity-plugin-internationalized-array";const version="v0";const preload=fn=>suspend.preload(()=>fn(),[version,namespace]);const clear=()=>suspend.clear([version,namespace]);const peek=selectedValue=>suspend.peek([version,namespace,selectedValue]);var Preload=memo(function Preload(props){const client=useClient({apiVersion:props.apiVersion});if(!Array.isArray(peek({}))){preload(async()=>Array.isArray(props.languages)?props.languages:props.languages(client,{}));}return null;});function camelCase(string){return string.replace(/-([a-z])/g,g=>g[1].toUpperCase());}function titleCase(string){return string.split(" ").map(word=>word.charAt(0).toUpperCase()+word.slice(1)).join(" ");}function pascalCase(string){return titleCase(camelCase(string));}function createFieldName(name){let addValue=arguments.length>1&&arguments[1]!==undefined?arguments[1]:false;return addValue?["internationalizedArray",pascalCase(name),"Value"].join(""):["internationalizedArray",pascalCase(name)].join("");}var commonjsGlobal=typeof globalThis!=='undefined'?globalThis:typeof window!=='undefined'?window:typeof global!=='undefined'?global:typeof self!=='undefined'?self:{};var lodash={exports:{}};/**
1
+ import*as suspend from'suspend-react';import{suspend as suspend$1}from'suspend-react';import{jsx,jsxs,Fragment}from'react/jsx-runtime';import{useClient,useFormBuilder,insert,defineDocumentFieldAction,useFormValue,PatchEvent,setIfMissing,set,ArrayOfObjectsItem,defineField,unset,definePlugin,isObjectInputProps}from'sanity';import{useLanguageFilterStudioContext}from'@sanity/language-filter';import equal from'fast-deep-equal';import{createContext,useContext,useDeferredValue,useMemo,memo,useCallback,useEffect}from'react';import{TranslateIcon,AddIcon,RemoveCircleIcon}from'@sanity/icons';import{useDocumentPane}from'sanity/desk';import{Card,Stack,Text,Code,useToast,Grid,Button,Spinner,Label,MenuButton,Menu,MenuItem,Flex}from'@sanity/ui';const namespace="sanity-plugin-internationalized-array";const version="v0";const preload=fn=>suspend.preload(()=>fn(),[version,namespace]);const clear=()=>suspend.clear([version,namespace]);const peek=selectedValue=>suspend.peek([version,namespace,selectedValue]);const MAX_COLUMNS=7;const CONFIG_DEFAULT={languages:[],select:{},defaultLanguages:[],fieldTypes:[],apiVersion:"2022-11-27",buttonLocations:["field"],buttonAddAll:true};var commonjsGlobal=typeof globalThis!=='undefined'?globalThis:typeof window!=='undefined'?window:typeof global!=='undefined'?global:typeof self!=='undefined'?self:{};var lodash={exports:{}};/**
2
2
  * @license
3
3
  * Lodash <https://lodash.com/>
4
4
  * Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
@@ -9467,24 +9467,33 @@ var _=runInContext();// Some AMD build optimizers, like r.js, check for conditio
9467
9467
  if(freeModule){// Export for Node.js.
9468
9468
  (freeModule.exports=_)._=_;// Export for CommonJS support.
9469
9469
  freeExports._=_;}else{// Export to the global object.
9470
- root._=_;}}).call(commonjsGlobal);})(lodash,lodash.exports);var lodashExports=lodash.exports;const getSelectedValue=(select,document)=>{if(!select||!document){return{};}const selection=select||{};const selectedValue={};for(const[key,path]of Object.entries(selection)){let value=lodashExports.get(document,path);if(Array.isArray(value)){value=value.filter(item=>typeof item==="object"?(item==null?void 0:item._type)==="reference"&&"_ref"in item:true);}selectedValue[key]=value;}return selectedValue;};const schemaExample={languages:[{id:"en",title:"English"},{id:"no",title:"Norsk"}]};function Feedback(){return/* @__PURE__ */jsx(Card,{tone:"caution",border:true,radius:2,padding:3,children:/* @__PURE__ */jsxs(Stack,{space:4,children:[/* @__PURE__ */jsxs(Text,{children:["An array of language objects must be passed into the"," ",/* @__PURE__ */jsx("code",{children:"internationalizedArray"})," helper function, each with an"," ",/* @__PURE__ */jsx("code",{children:"id"})," and ",/* @__PURE__ */jsx("code",{children:"title"})," field. Example:"]}),/* @__PURE__ */jsx(Card,{padding:2,border:true,radius:2,children:/* @__PURE__ */jsx(Code,{size:1,language:"javascript",children:JSON.stringify(schemaExample,null,2)})})]})});}const LanguageContext=React.createContext({languages:[]});const LanguageProvider=LanguageContext.Provider;function InternationalizedArray(props){const{members,value,schemaType,onChange}=props;const readOnly=typeof schemaType.readOnly==="boolean"?schemaType.readOnly:false;const{options}=schemaType;const toast=useToast();const{value:document}=useFormBuilder();const deferredDocument=useDeferredValue(document);const selectedValue=useMemo(()=>getSelectedValue(options.select,deferredDocument),[options.select,deferredDocument]);const{apiVersion}=options;const client=useClient({apiVersion});const languages=Array.isArray(options.languages)?options.languages:suspend$1(// eslint-disable-next-line require-await
9471
- async()=>{if(typeof options.languages==="function"){return options.languages(client,selectedValue);}return options.languages;},[version,namespace,selectedValue],{equal});const handleAddLanguage=useCallback(languageId=>{if(!(languages==null?void 0:languages.length)){return;}const itemBase={_type:"".concat(schemaType.name,"Value")};const newItems=languageId?// Just one for this language
9472
- [{...itemBase,_key:languageId}]:// Or one for every missing language
9473
- languages.filter(language=>(value==null?void 0:value.length)?!value.find(v=>v._key===language.id):true).map(language=>({...itemBase,_key:language.id}));const languagesInUse2=(value==null?void 0:value.length)?value.map(v=>v):[];const insertions=newItems.map(item=>{const languageIndex=languages.findIndex(l=>item._key===l.id);const remainingLanguages=languages.slice(languageIndex+1);const nextLanguageIndex=languagesInUse2.findIndex(l=>// eslint-disable-next-line max-nested-callbacks
9474
- remainingLanguages.find(r=>r.id===l._key));if(nextLanguageIndex<0){languagesInUse2.push(item);}else{languagesInUse2.splice(nextLanguageIndex,0,item);}return nextLanguageIndex<0?// No next language (-1), add to end of array
9475
- insert([item],"after",[nextLanguageIndex]):// Next language found, insert before that
9476
- insert([item],"before",[nextLanguageIndex]);});onChange([setIfMissing([]),...insertions]);},[languages,onChange,schemaType.name,value]);const handleRestoreOrder=useCallback(()=>{if(!(value==null?void 0:value.length)||!(languages==null?void 0:languages.length)){return;}const updatedValue=value.reduce((acc,v)=>{const newIndex=languages.findIndex(l=>l.id===(v==null?void 0:v._key));if(newIndex>-1){acc[newIndex]=v;}return acc;},[]).filter(Boolean);if((value==null?void 0:value.length)!==updatedValue.length){toast.push({title:"There was an error reordering languages",status:"warning"});}onChange(set(updatedValue));},[toast,languages,onChange,value]);const allKeysAreLanguages=useMemo(()=>{if(!(value==null?void 0:value.length)||!(languages==null?void 0:languages.length)){return true;}return value==null?void 0:value.every(v=>languages.find(l=>(l==null?void 0:l.id)===(v==null?void 0:v._key)));},[value,languages]);const languagesInUse=useMemo(()=>languages&&languages.length>1?languages.filter(l=>value==null?void 0:value.find(v=>v._key===l.id)):[],[languages,value]);const languagesOutOfOrder=useMemo(()=>{if(!(value==null?void 0:value.length)||!languagesInUse.length){return[];}return value.map((v,vIndex)=>vIndex===languagesInUse.findIndex(l=>l.id===v._key)?null:v).filter(Boolean);},[value,languagesInUse]);const languagesAreValid=useMemo(()=>!(languages==null?void 0:languages.length)||(languages==null?void 0:languages.length)&&languages.every(item=>item.id&&item.title),[languages]);useEffect(()=>{if(languagesOutOfOrder.length>0&&allKeysAreLanguages){handleRestoreOrder();}},[languagesOutOfOrder,allKeysAreLanguages,handleRestoreOrder]);if(!languagesAreValid){return/* @__PURE__ */jsx(Feedback,{});}return/* @__PURE__ */jsx(LanguageProvider,{value:{languages},children:/* @__PURE__ */jsxs(Stack,{space:2,children:[(members==null?void 0:members.length)>0?/* @__PURE__ */jsx(Fragment,{children:members.map(member=>{if(member.kind==="item"){return/* @__PURE__ */jsx(ArrayOfObjectsItem,{member,renderItem:props.renderItem,renderField:props.renderField,renderInput:props.renderInput,renderPreview:props.renderPreview},member.key);}return null;})}):null,(languages==null?void 0:languages.length)>0&&languagesInUse.length<languages.length?/* @__PURE__ */jsxs(Stack,{space:2,children:[languages.length>1?/* @__PURE__ */jsx(Grid,{columns:Math.min(languages.length,5),gap:2,children:languages.map(language=>/* @__PURE__ */jsx(Button,{tone:"primary",mode:"ghost",fontSize:1,disabled:readOnly||Boolean(value==null?void 0:value.find(item=>item._key===language.id)),text:language.id.toUpperCase(),icon:AddIcon,onClick:()=>handleAddLanguage(language.id)},language.id))}):null,/* @__PURE__ */jsx(Button,{tone:"primary",mode:"ghost",disabled:readOnly||value&&(value==null?void 0:value.length)>=(languages==null?void 0:languages.length),icon:AddIcon,text:// eslint-disable-next-line no-nested-ternary
9477
- (value==null?void 0:value.length)?"Add missing ".concat(languages.length-value.length===1?"language":"languages"):languages.length===1?"Add ".concat(languages[0].title," Field"):"Add all languages",onClick:()=>handleAddLanguage()})]}):null]})});}var array=config=>{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:{apiVersion,select,languages},// TODO: Resolve this typing issue with the inner object
9470
+ root._=_;}}).call(commonjsGlobal);})(lodash,lodash.exports);var lodashExports=lodash.exports;const getSelectedValue=(select,document)=>{if(!select||!document){return{};}const selection=select||{};const selectedValue={};for(const[key,path]of Object.entries(selection)){let value=lodashExports.get(document,path);if(Array.isArray(value)){value=value.filter(item=>typeof item==="object"?(item==null?void 0:item._type)==="reference"&&"_ref"in item:true);}selectedValue[key]=value;}return selectedValue;};const InternationalizedArrayContext=createContext({...CONFIG_DEFAULT,languages:[],filteredLanguages:[]});function useInternationalizedArrayContext(){return useContext(InternationalizedArrayContext);}function InternationalizedArrayProvider(props){const{internationalizedArray}=props;const client=useClient({apiVersion:internationalizedArray.apiVersion});const{value:document}=useFormBuilder();const deferredDocument=useDeferredValue(document);const selectedValue=useMemo(()=>getSelectedValue(internationalizedArray.select,deferredDocument),[internationalizedArray.select,deferredDocument]);const languages=Array.isArray(internationalizedArray.languages)?internationalizedArray.languages:suspend$1(// eslint-disable-next-line require-await
9471
+ async()=>{if(typeof internationalizedArray.languages==="function"){return internationalizedArray.languages(client,selectedValue);}return internationalizedArray.languages;},[version,namespace],{equal});const{selectedLanguageIds,options:languageFilterOptions}=useLanguageFilterStudioContext();const filteredLanguages=useMemo(()=>{const documentType=deferredDocument?deferredDocument._type:void 0;const languageFilterEnabled=typeof documentType==="string"&&languageFilterOptions.documentTypes.includes(documentType);return languageFilterEnabled?languages.filter(language=>selectedLanguageIds.includes(language.id)):languages;},[deferredDocument,languageFilterOptions,languages,selectedLanguageIds]);return/* @__PURE__ */jsx(InternationalizedArrayContext.Provider,{value:{...internationalizedArray,languages,filteredLanguages},children:props.renderDefault(props)});}var Preload=memo(function Preload(props){const client=useClient({apiVersion:props.apiVersion});if(!Array.isArray(peek({}))){preload(async()=>Array.isArray(props.languages)?props.languages:props.languages(client,{}));}return null;});function checkAllLanguagesArePresent(languages,value){const filteredLanguageIds=languages.map(l=>l.id);const languagesInUseIds=value?value.map(v=>v._key):[];return languagesInUseIds.length===filteredLanguageIds.length&&languagesInUseIds.every(l=>filteredLanguageIds.includes(l));}function createAddAllTitle(value,languages){if(value==null?void 0:value.length){return"Add missing ".concat(languages.length-value.length===1?"language":"languages");}return languages.length===1?"Add ".concat(languages[0].title," Field"):"Add all languages";}function createAddLanguagePatches(config){const{addLanguageKeys,schemaType,languages,filteredLanguages,value,path=[]}=config;const itemBase={_type:"".concat(schemaType.name,"Value")};const newItems=Array.isArray(addLanguageKeys)&&addLanguageKeys.length>0?// Just one for this language
9472
+ addLanguageKeys.map(id=>({...itemBase,_key:id})):// Or one for every missing language
9473
+ filteredLanguages.filter(language=>(value==null?void 0:value.length)?!value.find(v=>v._key===language.id):true).map(language=>({...itemBase,_key:language.id}));const languagesInUse=(value==null?void 0:value.length)?value.map(v=>v):[];const insertions=newItems.map(item=>{const languageIndex=languages.findIndex(l=>item._key===l.id);const remainingLanguages=languages.slice(languageIndex+1);const nextLanguageIndex=languagesInUse.findIndex(l=>// eslint-disable-next-line max-nested-callbacks
9474
+ remainingLanguages.find(r=>r.id===l._key));if(nextLanguageIndex<0){languagesInUse.push(item);}else{languagesInUse.splice(nextLanguageIndex,0,item);}return nextLanguageIndex<0?// No next language (-1), add to end of array
9475
+ insert([item],"after",[...path,nextLanguageIndex]):// Next language found, insert before that
9476
+ insert([item],"before",[...path,nextLanguageIndex]);});return insertions;}const createTranslateFieldActions=(fieldActionProps,_ref)=>{let{languages,filteredLanguages}=_ref;return languages.map(language=>{const value=useFormValue(fieldActionProps.path);const disabled=value&&Array.isArray(value)?Boolean(value==null?void 0:value.find(item=>item._key===language.id)):true;const hidden=!filteredLanguages.some(f=>f.id===language.id);const{onChange}=useDocumentPane();const onAction=useCallback(()=>{const{schemaType,path}=fieldActionProps;const addLanguageKeys=[language.id];const patches=createAddLanguagePatches({addLanguageKeys,schemaType,languages,filteredLanguages,value,path});onChange(PatchEvent.from([setIfMissing([],path),...patches]));},[language.id,value,onChange]);return{type:"action",icon:AddIcon,onAction,title:language.id.toLocaleUpperCase(),hidden,disabled};});};const AddMissingTranslationsFieldAction=(fieldActionProps,_ref2)=>{let{languages,filteredLanguages}=_ref2;const value=useFormValue(fieldActionProps.path);const disabled=value.length===filteredLanguages.length;const hidden=checkAllLanguagesArePresent(filteredLanguages,value);const{onChange}=useDocumentPane();const onAction=useCallback(()=>{const{schemaType,path}=fieldActionProps;const addLanguageKeys=[];const patches=createAddLanguagePatches({addLanguageKeys,schemaType,languages,filteredLanguages,value,path});onChange(PatchEvent.from([setIfMissing([],path),...patches]));},[fieldActionProps,filteredLanguages,languages,onChange,value]);return{type:"action",icon:AddIcon,onAction,title:createAddAllTitle(value,filteredLanguages),disabled,hidden};};const internationalizedArrayFieldAction=defineDocumentFieldAction({name:"internationalizedArray",useAction(fieldActionProps){var _a,_b;const isInternationalizedArrayField=(_b=(_a=fieldActionProps==null?void 0:fieldActionProps.schemaType)==null?void 0:_a.type)==null?void 0:_b.name.startsWith("internationalizedArray");const{languages,filteredLanguages}=useInternationalizedArrayContext();const translateFieldActions=createTranslateFieldActions(fieldActionProps,{languages,filteredLanguages});return{type:"group",icon:TranslateIcon,title:"Add Translation",renderAsButton:true,children:isInternationalizedArrayField?[...translateFieldActions,AddMissingTranslationsFieldAction(fieldActionProps,{languages,filteredLanguages})]:[],hidden:!isInternationalizedArrayField};}});function camelCase(string){return string.replace(/-([a-z])/g,g=>g[1].toUpperCase());}function titleCase(string){return string.split(" ").map(word=>word.charAt(0).toUpperCase()+word.slice(1)).join(" ");}function pascalCase(string){return titleCase(camelCase(string));}function createFieldName(name){let addValue=arguments.length>1&&arguments[1]!==undefined?arguments[1]:false;return addValue?["internationalizedArray",pascalCase(name),"Value"].join(""):["internationalizedArray",pascalCase(name)].join("");}const schemaExample={languages:[{id:"en",title:"English"},{id:"no",title:"Norsk"}]};function Feedback(){return/* @__PURE__ */jsx(Card,{tone:"caution",border:true,radius:2,padding:3,children:/* @__PURE__ */jsxs(Stack,{space:4,children:[/* @__PURE__ */jsxs(Text,{children:["An array of language objects must be passed into the"," ",/* @__PURE__ */jsx("code",{children:"internationalizedArray"})," helper function, each with an"," ",/* @__PURE__ */jsx("code",{children:"id"})," and ",/* @__PURE__ */jsx("code",{children:"title"})," field. Example:"]}),/* @__PURE__ */jsx(Card,{padding:2,border:true,radius:2,children:/* @__PURE__ */jsx(Code,{size:1,language:"javascript",children:JSON.stringify(schemaExample,null,2)})})]})});}function InternationalizedArray(props){const{members,value,schemaType,onChange}=props;const readOnly=typeof schemaType.readOnly==="boolean"?schemaType.readOnly:false;const toast=useToast();const{languages,filteredLanguages,defaultLanguages,buttonAddAll,buttonLocations}=useInternationalizedArrayContext();const{selectedLanguageIds,options:languageFilterOptions}=useLanguageFilterStudioContext();const documentType=useFormValue(["_type"]);const languageFilterEnabled=typeof documentType==="string"&&languageFilterOptions.documentTypes.includes(documentType);const filteredMembers=useMemo(()=>languageFilterEnabled?members.filter(member=>{if(member.kind!=="item"){return false;}const valueMember=member.item.members[0];if(valueMember.kind!=="field"){return false;}return languageFilterOptions.filterField(member.item.schemaType,valueMember,selectedLanguageIds);}):members,[languageFilterEnabled,members,languageFilterOptions,selectedLanguageIds]);const handleAddLanguage=useCallback(param=>{var _a;if(!(filteredLanguages==null?void 0:filteredLanguages.length)){return;}const addLanguageKeys=Array.isArray(param)?param:[(_a=param==null?void 0:param.currentTarget)==null?void 0:_a.value].filter(Boolean);const patches=createAddLanguagePatches({addLanguageKeys,schemaType,languages,filteredLanguages,value});onChange([setIfMissing([]),...patches]);},[filteredLanguages,languages,onChange,schemaType,value]);const documentCreatedAt=useFormValue(["_createdAt"]);if(// Array field is empty
9477
+ !value&&// Document form is in "not yet created" state
9478
+ !documentCreatedAt&&// Plugin config included default languages
9479
+ defaultLanguages&&(defaultLanguages==null?void 0:defaultLanguages.length)>0){handleAddLanguage(defaultLanguages);}const handleRestoreOrder=useCallback(()=>{if(!(value==null?void 0:value.length)||!(languages==null?void 0:languages.length)){return;}const updatedValue=value.reduce((acc,v)=>{const newIndex=languages.findIndex(l=>l.id===(v==null?void 0:v._key));if(newIndex>-1){acc[newIndex]=v;}return acc;},[]).filter(Boolean);if((value==null?void 0:value.length)!==updatedValue.length){toast.push({title:"There was an error reordering languages",status:"warning"});}onChange(set(updatedValue));},[toast,languages,onChange,value]);const allKeysAreLanguages=useMemo(()=>{if(!(value==null?void 0:value.length)||!(languages==null?void 0:languages.length)){return true;}return value==null?void 0:value.every(v=>languages.find(l=>(l==null?void 0:l.id)===(v==null?void 0:v._key)));},[value,languages]);const languagesInUse=useMemo(()=>languages&&languages.length>1?languages.filter(l=>value==null?void 0:value.find(v=>v._key===l.id)):[],[languages,value]);const languagesOutOfOrder=useMemo(()=>{if(!(value==null?void 0:value.length)||!languagesInUse.length){return[];}return value.map((v,vIndex)=>vIndex===languagesInUse.findIndex(l=>l.id===v._key)?null:v).filter(Boolean);},[value,languagesInUse]);const languagesAreValid=useMemo(()=>!(languages==null?void 0:languages.length)||(languages==null?void 0:languages.length)&&languages.every(item=>item.id&&item.title),[languages]);useEffect(()=>{if(languagesOutOfOrder.length>0&&allKeysAreLanguages){handleRestoreOrder();}},[languagesOutOfOrder,allKeysAreLanguages,handleRestoreOrder]);const allLanguagesArePresent=useMemo(()=>checkAllLanguagesArePresent(filteredLanguages,value),[filteredLanguages,value]);if(!languagesAreValid){return/* @__PURE__ */jsx(Feedback,{});}const addButtonsAreVisible=// Plugin was configured to display buttons here (default!)
9480
+ buttonLocations.includes("field")&&// There's at least one language visible
9481
+ (filteredLanguages==null?void 0:filteredLanguages.length)>0&&// Not every language has a value yet
9482
+ !allLanguagesArePresent;return/* @__PURE__ */jsxs(Stack,{space:2,children:[(members==null?void 0:members.length)>0?/* @__PURE__ */jsx(Fragment,{children:filteredMembers.map(member=>{if(member.kind==="item"){return/* @__PURE__ */jsx(ArrayOfObjectsItem,{member,renderItem:props.renderItem,renderField:props.renderField,renderInput:props.renderInput,renderPreview:props.renderPreview},member.key);}return null;})}):null,addButtonsAreVisible?/* @__PURE__ */jsxs(Stack,{space:2,children:[filteredLanguages.length>1?/* @__PURE__ */jsx(Grid,{columns:Math.min(filteredLanguages.length,MAX_COLUMNS),gap:2,children:filteredLanguages.map(language=>/* @__PURE__ */jsx(Button,{tone:"primary",mode:"ghost",fontSize:1,disabled:readOnly||Boolean(value==null?void 0:value.find(item=>item._key===language.id)),text:language.id.toUpperCase(),icon:filteredLanguages.length>MAX_COLUMNS?void 0:AddIcon,value:language.id,onClick:handleAddLanguage},language.id))}):null,buttonAddAll?/* @__PURE__ */jsx(Button,{tone:"primary",mode:"ghost",disabled:readOnly||allLanguagesArePresent,icon:AddIcon,text:createAddAllTitle(value,filteredLanguages),onClick:handleAddLanguage}):null]}):null]});}var array=config=>{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},// These options are required for validation rules – not the custom input component
9483
+ options:{apiVersion,select,languages},// TODO: Resolve this typing issue with the inner object
9478
9484
  // @ts-expect-error
9479
9485
  of:[defineField({...(typeof type==="string"?{}:type),name:objectName,type:objectName})],validation:rule=>rule.custom(async(value,context)=>{var _a,_b,_c;if(!value){return true;}const selectedValue=getSelectedValue(select,context.document);const client=context.getClient({apiVersion});const contextLanguages=Array.isArray((_b=(_a=context==null?void 0:context.type)==null?void 0:_a.options)==null?void 0:_b.languages)?context.type.options.languages:Array.isArray(peek(selectedValue))?peek(selectedValue):await((_c=context==null?void 0:context.type)==null?void 0:_c.options.languages(client,selectedValue));if(value&&value.length>contextLanguages.length){return"Cannot be more than ".concat(contextLanguages.length===1?"1 item":"".concat(contextLanguages.length," items"));}const nonLanguageKeys=(value==null?void 0: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}])};}const valuesByLanguage=(value==null?void 0:value.length)?value.filter(item=>Boolean(item==null?void 0:item._key)).reduce((acc,cur)=>{if(acc[cur._key]){return{...acc,[cur._key]:[...acc[cur._key],cur]};}return{...acc,[cur._key]:[cur]};},{}):{};const duplicateValues=Object.values(valuesByLanguage).filter(item=>(item==null?void 0: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;})});};function InternationalizedField(props){if(props.schemaType.name==="reference"&&props.value){return props.renderDefault({...props,title:"",level:0});}return props.children;}function getToneFromValidation(validations){if(!(validations==null?void 0:validations.length)){return void 0;}const validationLevels=validations.map(v=>v.level);if(validationLevels.includes("error")){return"critical";}else if(validationLevels.includes("warning")){return"caution";}return void 0;}function InternationalizedInput(props){const parentValue=useFormValue(props.path.slice(0,-1));const inlineProps={...props.inputProps,// This is the magic that makes inline editing work?
9480
9486
  members:props.inputProps.members.filter(m=>m.kind==="field"&&m.name==="value"),// This just overrides the type
9481
9487
  // TODO: Remove this as it shouldn't be necessary?
9482
- value:props.value};const{validation,value,onChange,readOnly}=inlineProps;const{languages}=React.useContext(LanguageContext);const languageKeysInUse=useMemo(()=>{var _a;return(_a=parentValue==null?void 0:parentValue.map(v=>v._key))!=null?_a:[];},[parentValue]);const keyIsValid=(languages==null?void 0:languages.length)?languages.find(l=>l.id===value._key):false;const handleKeyChange=useCallback(languageId=>{if(!value||!(languages==null?void 0:languages.length)||!languages.find(l=>l.id===languageId)){return;}onChange([set(languageId,["_key"])]);},[onChange,value,languages]);const handleUnset=useCallback(()=>{onChange(unset());},[onChange]);if(!languages){return/* @__PURE__ */jsx(Spinner,{});}return/* @__PURE__ */jsx(Card,{paddingTop:2,tone:getToneFromValidation(validation),children:/* @__PURE__ */jsxs(Stack,{space:2,children:[/* @__PURE__ */jsx(Card,{tone:"inherit",children:keyIsValid?/* @__PURE__ */jsx(Label,{muted:true,size:1,children:value._key}):/* @__PURE__ */jsx(MenuButton,{button:/* @__PURE__ */jsx(Button,{fontSize:1,text:"Change \"".concat(value._key,"\"")}),id:"".concat(value._key,"-change-key"),menu:/* @__PURE__ */jsx(Menu,{children:languages.map(language=>/* @__PURE__ */jsx(MenuItem,{disabled:languageKeysInUse.includes(language.id),fontSize:1,text:language.id.toLocaleUpperCase(),onClick:()=>handleKeyChange(language.id)},language.id))}),placement:"right",popover:{portal:true}})}),/* @__PURE__ */jsxs(Flex,{align:"center",gap:2,children:[/* @__PURE__ */jsx(Card,{flex:1,tone:"inherit",children:props.inputProps.renderInput(props.inputProps)}),/* @__PURE__ */jsx(Card,{tone:"inherit",children:/* @__PURE__ */jsx(Button,{mode:"bleed",icon:RemoveCircleIcon,tone:"critical",disabled:readOnly,onClick:handleUnset})})]})]})});}var object=config=>{const{type}=config;const typeName=typeof type==="string"?type:type.name;const objectName=createFieldName(typeName,true);return defineField({name:objectName,title:"Internationalized array ".concat(type),type:"object",components:{item:InternationalizedInput},// TODO: Address this typing issue with the inner object
9488
+ value:props.value};const{validation,value,onChange,readOnly}=inlineProps;const{languages}=useInternationalizedArrayContext();const languageKeysInUse=useMemo(()=>{var _a;return(_a=parentValue==null?void 0:parentValue.map(v=>v._key))!=null?_a:[];},[parentValue]);const keyIsValid=(languages==null?void 0:languages.length)?languages.find(l=>l.id===value._key):false;const handleKeyChange=useCallback(event=>{var _a;const languageId=(_a=event==null?void 0:event.currentTarget)==null?void 0:_a.value;if(!value||!(languages==null?void 0:languages.length)||!languages.find(l=>l.id===languageId)){return;}onChange([set(languageId,["_key"])]);},[onChange,value,languages]);const handleUnset=useCallback(()=>{onChange(unset());},[onChange]);if(!languages){return/* @__PURE__ */jsx(Spinner,{});}return/* @__PURE__ */jsx(Card,{paddingTop:2,tone:getToneFromValidation(validation),children:/* @__PURE__ */jsxs(Stack,{space:2,children:[/* @__PURE__ */jsx(Card,{tone:"inherit",children:keyIsValid?/* @__PURE__ */jsx(Label,{muted:true,size:1,children:value._key}):/* @__PURE__ */jsx(MenuButton,{button:/* @__PURE__ */jsx(Button,{fontSize:1,text:"Change \"".concat(value._key,"\"")}),id:"".concat(value._key,"-change-key"),menu:/* @__PURE__ */jsx(Menu,{children:languages.map(language=>/* @__PURE__ */jsx(MenuItem,{disabled:languageKeysInUse.includes(language.id),fontSize:1,text:language.id.toLocaleUpperCase(),value:language.id,onClick:handleKeyChange},language.id))}),popover:{portal:true}})}),/* @__PURE__ */jsxs(Flex,{align:"center",gap:2,children:[/* @__PURE__ */jsx(Card,{flex:1,tone:"inherit",children:props.inputProps.renderInput(props.inputProps)}),/* @__PURE__ */jsx(Card,{tone:"inherit",children:/* @__PURE__ */jsx(Button,{mode:"bleed",icon:RemoveCircleIcon,tone:"critical",disabled:readOnly,onClick:handleUnset})})]})]})});}var object=config=>{const{type}=config;const typeName=typeof type==="string"?type:type.name;const objectName=createFieldName(typeName,true);return defineField({name:objectName,title:"Internationalized array ".concat(type),type:"object",components:{item:InternationalizedInput},// TODO: Address this typing issue with the inner object
9483
9489
  // @ts-expect-error
9484
9490
  fields:[typeof type==="string"?// Define a simple field if all we have is the name as a string
9485
9491
  defineField({name:"value",type,components:{// TODO: Address this typing issue with the inner object
9486
9492
  // @ts-expect-error
9487
9493
  field:InternationalizedField}}):// Pass in the configured options, but overwrite the name
9488
- {...type,name:"value",components:{field:InternationalizedField}}],preview:{select:{title:"value",subtitle:"_key"}}});};const CONFIG_DEFAULT={languages:[],fieldTypes:[]};const internationalizedArray=definePlugin(function(){let config=arguments.length>0&&arguments[0]!==undefined?arguments[0]:CONFIG_DEFAULT;const{apiVersion="2022-11-27",select,languages,fieldTypes}={...CONFIG_DEFAULT,...config};return{name:"sanity-plugin-internationalized-array",// If `languages` is a callback then let's preload it
9489
- studio:Array.isArray(languages)?void 0:{components:{layout:props=>/* @__PURE__ */jsxs(Fragment,{children:[/* @__PURE__ */jsx(Preload,{apiVersion,languages}),props.renderDefault(props)]})}},schema:{types:[...fieldTypes.map(type=>array({type,apiVersion,select,languages})),...fieldTypes.map(type=>object({type}))]}};});export{clear,internationalizedArray};
9494
+ {...type,name:"value",components:{field:InternationalizedField}}],preview:{select:{title:"value",subtitle:"_key"}}});};const internationalizedArray=definePlugin(config=>{const pluginConfig={...CONFIG_DEFAULT,...config};const{apiVersion="2022-11-27",select,languages,fieldTypes,defaultLanguages,buttonLocations}=pluginConfig;return{name:"sanity-plugin-internationalized-array",// Preload languages for use throughout the Studio
9495
+ studio:Array.isArray(languages)?void 0:{components:{layout:props=>/* @__PURE__ */jsxs(Fragment,{children:[/* @__PURE__ */jsx(Preload,{apiVersion,languages}),props.renderDefault(props)]})}},// Optional: render "add language" buttons as field actions
9496
+ document:{unstable_fieldActions:buttonLocations.includes("unstable__fieldAction")?prev=>[...prev,internationalizedArrayFieldAction]:void 0},// Wrap document editor with a language provider
9497
+ form:{components:{input:props=>{const isRootInput=props.id==="root"&&isObjectInputProps(props);if(!isRootInput){return props.renderDefault(props);}const rootFieldTypeNames=props.schemaType.fields.map(field=>field.type.name);const hasInternationalizedArray=rootFieldTypeNames.some(name=>name.startsWith("internationalizedArray"));if(!hasInternationalizedArray){return props.renderDefault(props);}return InternationalizedArrayProvider({...props,internationalizedArray:pluginConfig});}}},// Register custom schema types for the outer array and the inner object
9498
+ schema:{types:[...fieldTypes.map(type=>array({type,apiVersion,select,languages,defaultLanguages})),...fieldTypes.map(type=>object({type}))]}};});export{clear,internationalizedArray};
9490
9499
  //# sourceMappingURL=index.esm.js.map