sanity-plugin-internationalized-array 3.2.2 → 4.0.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.
Files changed (43) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +3 -34
  3. package/{lib → dist}/index.d.ts +41 -61
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +882 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +36 -74
  8. package/lib/index.d.mts +0 -149
  9. package/lib/index.esm.js +0 -854
  10. package/lib/index.esm.js.map +0 -1
  11. package/lib/index.js +0 -863
  12. package/lib/index.js.map +0 -1
  13. package/lib/index.mjs +0 -854
  14. package/lib/index.mjs.map +0 -1
  15. package/sanity.json +0 -8
  16. package/src/cache.ts +0 -148
  17. package/src/components/AddButtons.tsx +0 -60
  18. package/src/components/DocumentAddButtons.tsx +0 -183
  19. package/src/components/Feedback.tsx +0 -28
  20. package/src/components/InternationalizedArray.tsx +0 -286
  21. package/src/components/InternationalizedArrayContext.tsx +0 -136
  22. package/src/components/InternationalizedField.tsx +0 -57
  23. package/src/components/InternationalizedInput.tsx +0 -257
  24. package/src/components/Preload.tsx +0 -31
  25. package/src/components/createFieldName.ts +0 -20
  26. package/src/components/getSelectedValue.ts +0 -31
  27. package/src/components/getToneFromValidation.ts +0 -20
  28. package/src/constants.ts +0 -18
  29. package/src/fieldActions/index.ts +0 -138
  30. package/src/index.ts +0 -3
  31. package/src/plugin.tsx +0 -87
  32. package/src/schema/array.ts +0 -148
  33. package/src/schema/object.ts +0 -36
  34. package/src/types.ts +0 -135
  35. package/src/utils/checkAllLanguagesArePresent.ts +0 -14
  36. package/src/utils/createAddAllTitle.ts +0 -16
  37. package/src/utils/createAddLanguagePatches.ts +0 -84
  38. package/src/utils/createValueSchemaTypeName.ts +0 -5
  39. package/src/utils/flattenSchemaType.ts +0 -63
  40. package/src/utils/getDocumentsToTranslate.ts +0 -66
  41. package/src/utils/getLanguageDisplay.ts +0 -13
  42. package/src/utils/getLanguagesFieldOption.ts +0 -16
  43. package/v2-incompatible.js +0 -11
package/lib/index.js DELETED
@@ -1,863 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: !0 });
3
- var suspend = require("suspend-react"), jsxRuntime = require("react/jsx-runtime"), sanity = require("sanity"), languageFilter = require("@sanity/language-filter"), ui = require("@sanity/ui"), equal = require("fast-deep-equal"), react = require("react"), structure = require("sanity/structure"), icons = require("@sanity/icons"), get = require("lodash/get.js");
4
- function _interopDefaultCompat(e) {
5
- return e && typeof e == "object" && "default" in e ? e : { default: e };
6
- }
7
- function _interopNamespaceCompat(e) {
8
- if (e && typeof e == "object" && "default" in e) return e;
9
- var n = /* @__PURE__ */ Object.create(null);
10
- return e && Object.keys(e).forEach(function(k) {
11
- if (k !== "default") {
12
- var d = Object.getOwnPropertyDescriptor(e, k);
13
- Object.defineProperty(n, k, d.get ? d : {
14
- enumerable: !0,
15
- get: function() {
16
- return e[k];
17
- }
18
- });
19
- }
20
- }), n.default = e, Object.freeze(n);
21
- }
22
- var suspend__namespace = /* @__PURE__ */ _interopNamespaceCompat(suspend), equal__default = /* @__PURE__ */ _interopDefaultCompat(equal), get__default = /* @__PURE__ */ _interopDefaultCompat(get);
23
- const namespace = "sanity-plugin-internationalized-array", version = "v1", functionCache = /* @__PURE__ */ new Map(), functionKeyCache = /* @__PURE__ */ new WeakMap(), preloadWithKey = (fn, key) => suspend__namespace.preload(() => fn(), key), clear = () => suspend__namespace.clear([version, namespace]), peek = (selectedValue) => suspend__namespace.peek([version, namespace, selectedValue]), createCacheKey = (selectedValue, workspaceId) => {
24
- const selectedValueHash = JSON.stringify(selectedValue);
25
- return workspaceId ? [version, namespace, selectedValueHash, workspaceId] : [version, namespace, selectedValueHash];
26
- }, getFunctionKey = (fn) => {
27
- const cachedKey = functionKeyCache.get(fn);
28
- if (cachedKey)
29
- return cachedKey;
30
- const fnStr = fn.toString();
31
- let hash = 0;
32
- const maxLength = Math.min(fnStr.length, 100);
33
- for (let i = 0; i < maxLength; i++) {
34
- const char = fnStr.charCodeAt(i);
35
- hash = (hash << 5) - hash + char, hash &= hash;
36
- }
37
- const key = `anonymous_${Math.abs(hash)}`;
38
- return functionKeyCache.set(fn, key), key;
39
- }, createFunctionCacheKey = (fn, selectedValue, workspaceId) => {
40
- const functionKey = getFunctionKey(fn), selectedValueHash = JSON.stringify(selectedValue);
41
- return workspaceId ? `${functionKey}:${selectedValueHash}:${workspaceId}` : `${functionKey}:${selectedValueHash}`;
42
- }, getFunctionCache = (fn, selectedValue, workspaceId) => {
43
- const key = createFunctionCacheKey(fn, selectedValue, workspaceId);
44
- return functionCache.get(key);
45
- }, setFunctionCache = (fn, selectedValue, languages, workspaceId) => {
46
- const key = createFunctionCacheKey(fn, selectedValue, workspaceId);
47
- functionCache.set(key, languages);
48
- }, MAX_COLUMNS = {
49
- codeOnly: 5,
50
- titleOnly: 4,
51
- titleAndCode: 3
52
- }, CONFIG_DEFAULT = {
53
- languages: [],
54
- select: {},
55
- defaultLanguages: [],
56
- fieldTypes: [],
57
- apiVersion: "2025-10-15",
58
- buttonLocations: ["field"],
59
- buttonAddAll: !0,
60
- languageDisplay: "codeOnly"
61
- }, getDocumentsToTranslate = (value, rootPath = []) => {
62
- if (Array.isArray(value)) {
63
- const arrayRootPath = [...rootPath], internationalizedValues = value.filter((item) => {
64
- if (Array.isArray(item)) return !1;
65
- if (typeof item == "object") {
66
- const type = item?._type;
67
- return type?.startsWith("internationalizedArray") && type?.endsWith("Value");
68
- }
69
- return !1;
70
- });
71
- return internationalizedValues.length > 0 ? internationalizedValues.map((internationalizedValue) => ({
72
- ...internationalizedValue,
73
- path: arrayRootPath,
74
- pathString: arrayRootPath.join(".")
75
- })) : value.length > 0 ? value.map(
76
- (item, index) => getDocumentsToTranslate(item, [...arrayRootPath, index])
77
- ).flat() : [];
78
- }
79
- if (typeof value == "object" && value) {
80
- const startsWithUnderscoreRegex = /^_/;
81
- return Object.keys(value).filter(
82
- (key) => !key.match(startsWithUnderscoreRegex)
83
- ).map((item) => {
84
- const selectedValue = value[item], path = [...rootPath, item];
85
- return getDocumentsToTranslate(selectedValue, path);
86
- }).flat();
87
- }
88
- return [];
89
- };
90
- function getLanguageDisplay(languageDisplay, title, code) {
91
- return languageDisplay === "codeOnly" ? code.toUpperCase() : languageDisplay === "titleOnly" ? title : languageDisplay === "titleAndCode" ? `${title} (${code.toUpperCase()})` : title;
92
- }
93
- function AddButtons$1(props) {
94
- const { languages, readOnly, value, onClick } = props, { languageDisplay } = useInternationalizedArrayContext();
95
- return languages.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
96
- ui.Grid,
97
- {
98
- columns: Math.min(languages.length, MAX_COLUMNS[languageDisplay]),
99
- gap: 2,
100
- children: languages.map((language) => {
101
- const languageTitle = getLanguageDisplay(
102
- languageDisplay,
103
- language.title,
104
- language.id
105
- );
106
- return /* @__PURE__ */ jsxRuntime.jsx(
107
- ui.Button,
108
- {
109
- tone: "primary",
110
- mode: "ghost",
111
- fontSize: 1,
112
- disabled: readOnly || !!value?.find((item) => item._key === language.id),
113
- text: languageTitle,
114
- icon: languages.length > MAX_COLUMNS[languageDisplay] && languageDisplay === "codeOnly" ? void 0 : icons.AddIcon,
115
- value: language.id,
116
- onClick
117
- },
118
- language.id
119
- );
120
- })
121
- }
122
- ) : null;
123
- }
124
- var AddButtons = react.memo(AddButtons$1);
125
- function DocumentAddButtons(props) {
126
- const { filteredLanguages } = useInternationalizedArrayContext(), value = sanity.isSanityDocument(props.value) ? props.value : void 0, toast = ui.useToast(), { onChange } = structure.useDocumentPane(), schema = sanity.useSchema(), documentsToTranslation = getDocumentsToTranslate(value, []), getInitialValueForType = react.useCallback(
127
- (typeName) => {
128
- if (!typeName) return;
129
- const match = typeName.match(/^internationalizedArray(.+)Value$/);
130
- if (!match) return;
131
- const baseTypeName = match[1].charAt(0).toLowerCase() + match[1].slice(1), arrayBasedTypes = [
132
- "body",
133
- "htmlContent",
134
- "blockContent",
135
- "portableText"
136
- ];
137
- if (arrayBasedTypes.includes(baseTypeName))
138
- return [];
139
- try {
140
- const schemaType = schema.get(typeName);
141
- if (schemaType) {
142
- const valueField = schemaType?.fields?.find(
143
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
144
- (f) => f.name === "value"
145
- );
146
- if (valueField) {
147
- const fieldType = valueField.type;
148
- if (fieldType?.jsonType === "array" || fieldType?.name === "array" || fieldType?.type === "array" || fieldType?.of !== void 0 || arrayBasedTypes.includes(fieldType?.name))
149
- return [];
150
- }
151
- }
152
- } catch (error) {
153
- console.warn(
154
- "Could not determine field type from schema:",
155
- typeName,
156
- error
157
- );
158
- }
159
- },
160
- [schema]
161
- ), handleDocumentButtonClick = react.useCallback(
162
- async (event) => {
163
- const languageId = event.currentTarget.value;
164
- if (!languageId) {
165
- toast.push({
166
- status: "error",
167
- title: "No language selected"
168
- });
169
- return;
170
- }
171
- const alreadyTranslated = documentsToTranslation.filter(
172
- (translation) => translation?._key === languageId
173
- ), removeDuplicates = documentsToTranslation.reduce((filteredTranslations, translation) => alreadyTranslated.filter(
174
- (alreadyTranslation) => alreadyTranslation.pathString === translation.pathString
175
- ).length > 0 || filteredTranslations.filter(
176
- (filteredTranslation) => filteredTranslation.path === translation.path
177
- ).length > 0 ? filteredTranslations : [...filteredTranslations, translation], []);
178
- if (removeDuplicates.length === 0) {
179
- toast.push({
180
- status: "error",
181
- title: "No internationalizedArray fields found in document root"
182
- });
183
- return;
184
- }
185
- const patches = [];
186
- for (const toTranslate of removeDuplicates) {
187
- const path = toTranslate.path, initialValue = getInitialValueForType(toTranslate._type), ifMissing = sanity.setIfMissing([], path), insertValue = sanity.insert(
188
- [
189
- {
190
- _key: languageId,
191
- _type: toTranslate._type,
192
- value: initialValue
193
- // Use the determined initial value instead of undefined
194
- }
195
- ],
196
- "after",
197
- [...path, -1]
198
- );
199
- patches.push(ifMissing), patches.push(insertValue);
200
- }
201
- onChange(sanity.PatchEvent.from(patches.flat()));
202
- },
203
- [documentsToTranslation, getInitialValueForType, onChange, toast]
204
- );
205
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
206
- /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "semibold", children: "Add translation to internationalized fields" }) }),
207
- /* @__PURE__ */ jsxRuntime.jsx(
208
- AddButtons,
209
- {
210
- languages: filteredLanguages,
211
- readOnly: !1,
212
- value: void 0,
213
- onClick: handleDocumentButtonClick
214
- }
215
- )
216
- ] });
217
- }
218
- const getSelectedValue = (select, document) => {
219
- if (!select || !document)
220
- return {};
221
- const selection = select || {}, selectedValue = {};
222
- for (const [key, path] of Object.entries(selection)) {
223
- let value = get__default.default(document, path);
224
- Array.isArray(value) && (value = value.filter(
225
- (item) => typeof item == "object" ? item?._type === "reference" && "_ref" in item : !0
226
- )), selectedValue[key] = value;
227
- }
228
- return selectedValue;
229
- }, InternationalizedArrayContext = react.createContext({
230
- ...CONFIG_DEFAULT,
231
- languages: [],
232
- filteredLanguages: []
233
- });
234
- function useInternationalizedArrayContext() {
235
- return react.useContext(InternationalizedArrayContext);
236
- }
237
- function InternationalizedArrayProvider(props) {
238
- const { internationalizedArray: internationalizedArray2 } = props, client = sanity.useClient({ apiVersion: internationalizedArray2.apiVersion }), workspace = sanity.useWorkspace(), { formState } = structure.useDocumentPane(), deferredDocument = react.useDeferredValue(formState?.value), selectedValue = react.useMemo(
239
- () => getSelectedValue(internationalizedArray2.select, deferredDocument),
240
- [internationalizedArray2.select, deferredDocument]
241
- ), workspaceId = react.useMemo(() => {
242
- if (workspace?.name)
243
- return workspace.name;
244
- const workspaceKey = {
245
- name: workspace?.name,
246
- title: workspace?.title
247
- // Add other stable properties as needed
248
- };
249
- return JSON.stringify(workspaceKey);
250
- }, [workspace]), cacheKey = react.useMemo(
251
- () => createCacheKey(selectedValue, workspaceId),
252
- [selectedValue, workspaceId]
253
- ), languages = Array.isArray(internationalizedArray2.languages) ? internationalizedArray2.languages : suspend.suspend(
254
- // eslint-disable-next-line require-await
255
- async () => {
256
- if (typeof internationalizedArray2.languages == "function") {
257
- const result = await internationalizedArray2.languages(
258
- client,
259
- selectedValue
260
- );
261
- return setFunctionCache(
262
- internationalizedArray2.languages,
263
- selectedValue,
264
- result,
265
- workspaceId
266
- ), result;
267
- }
268
- return internationalizedArray2.languages;
269
- },
270
- cacheKey,
271
- { equal: equal__default.default }
272
- ), { selectedLanguageIds, options: languageFilterOptions } = languageFilter.useLanguageFilterStudioContext(), filteredLanguages = react.useMemo(() => {
273
- const documentType = deferredDocument ? deferredDocument._type : void 0;
274
- return typeof documentType == "string" && languageFilterOptions.documentTypes.includes(documentType) ? languages.filter(
275
- (language) => selectedLanguageIds.includes(language.id)
276
- ) : languages;
277
- }, [deferredDocument, languageFilterOptions, languages, selectedLanguageIds]), showDocumentButtons = internationalizedArray2.buttonLocations.includes("document"), context = react.useMemo(
278
- () => ({ ...internationalizedArray2, languages, filteredLanguages }),
279
- [filteredLanguages, internationalizedArray2, languages]
280
- );
281
- return /* @__PURE__ */ jsxRuntime.jsx(InternationalizedArrayContext.Provider, { value: context, children: showDocumentButtons ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 5, children: [
282
- /* @__PURE__ */ jsxRuntime.jsx(DocumentAddButtons, { value: props.value }),
283
- props.renderDefault(props)
284
- ] }) : props.renderDefault(props) });
285
- }
286
- function InternationalizedField(props) {
287
- const { languages } = useInternationalizedArrayContext(), customProps = react.useMemo(() => {
288
- const pathSegment = props.path.slice(0, -1)[1], languageId = typeof pathSegment == "object" && "_key" in pathSegment ? pathSegment._key : void 0, hasValidLanguageId = languageId ? languages.some((l) => l.id === languageId) : !1, shouldHideTitle = props.title?.toLowerCase() === "value" && hasValidLanguageId;
289
- return {
290
- ...props,
291
- title: shouldHideTitle ? "" : props.title
292
- };
293
- }, [props, languages]);
294
- return customProps.schemaType.name.startsWith("internationalizedArray") ? customProps.schemaType.name === "reference" && customProps.value ? customProps.renderDefault({
295
- ...customProps,
296
- title: "",
297
- level: 0
298
- // Reset the level to avoid nested styling
299
- }) : customProps.schemaType.name === "string" || customProps.schemaType.name === "number" || customProps.schemaType.name === "text" ? customProps.children : customProps.renderDefault({
300
- ...customProps,
301
- level: 0
302
- // Reset the level to avoid nested styling
303
- }) : customProps.renderDefault(customProps);
304
- }
305
- var Preload = react.memo(function(props) {
306
- const client = sanity.useClient({ apiVersion: props.apiVersion }), cacheKey = createCacheKey({});
307
- return Array.isArray(peek({})) || preloadWithKey(async () => {
308
- if (Array.isArray(props.languages))
309
- return props.languages;
310
- const result = await props.languages(client, {});
311
- return setFunctionCache(props.languages, {}, result), result;
312
- }, cacheKey), null;
313
- });
314
- function checkAllLanguagesArePresent(languages, value) {
315
- const filteredLanguageIds = languages.map((l) => l.id), languagesInUseIds = value ? value.map((v) => v._key) : [];
316
- return languagesInUseIds.length === filteredLanguageIds.length && languagesInUseIds.every((l) => filteredLanguageIds.includes(l));
317
- }
318
- function createAddAllTitle(value, languages) {
319
- return value?.length ? `Add missing ${languages.length - value.length === 1 ? "language" : "languages"}` : languages.length === 1 ? `Add ${languages[0].title} Field` : "Add all languages";
320
- }
321
- function createValueSchemaTypeName(schemaType) {
322
- return `${schemaType.name}Value`;
323
- }
324
- function createAddLanguagePatches(config) {
325
- const {
326
- addLanguageKeys,
327
- schemaType,
328
- languages,
329
- filteredLanguages,
330
- value,
331
- path = []
332
- } = config, itemBase = { _type: createValueSchemaTypeName(schemaType) }, newItems = Array.isArray(addLanguageKeys) && addLanguageKeys.length > 0 ? addLanguageKeys.map((id) => ({
333
- ...itemBase,
334
- _key: id
335
- })) : filteredLanguages.filter(
336
- (language) => value?.length ? !value.find((v) => v._key === language.id) : !0
337
- ).map((language) => ({
338
- ...itemBase,
339
- _key: language.id
340
- })), languagesInUse = value?.length ? value.map((v) => v) : [];
341
- return newItems.map((item) => {
342
- const languageIndex = languages.findIndex((l) => item._key === l.id), remainingLanguages = languages.slice(languageIndex + 1), nextLanguageIndex = languagesInUse.findIndex(
343
- (l) => (
344
- // eslint-disable-next-line max-nested-callbacks
345
- remainingLanguages.find((r) => r.id === l._key)
346
- )
347
- );
348
- return nextLanguageIndex < 0 ? languagesInUse.push(item) : languagesInUse.splice(nextLanguageIndex, 0, item), nextLanguageIndex < 0 ? (
349
- // No next language (-1), add to end of array
350
- sanity.insert([item], "after", [...path, nextLanguageIndex])
351
- ) : (
352
- // Next language found, insert before that
353
- sanity.insert([item], "before", [...path, nextLanguageIndex])
354
- );
355
- });
356
- }
357
- const createTranslateFieldActions = (fieldActionProps, { languages, filteredLanguages }) => languages.map((language) => {
358
- const value = sanity.useFormValue(fieldActionProps.path), disabled = value && Array.isArray(value) ? !!value?.find((item) => item._key === language.id) : !1, hidden = !filteredLanguages.some((f) => f.id === language.id), { onChange } = structure.useDocumentPane(), onAction = react.useCallback(() => {
359
- const { schemaType, path } = fieldActionProps, addLanguageKeys = [language.id], patches = createAddLanguagePatches({
360
- addLanguageKeys,
361
- schemaType,
362
- languages,
363
- filteredLanguages,
364
- value,
365
- path
366
- });
367
- onChange(sanity.PatchEvent.from([sanity.setIfMissing([], path), ...patches]));
368
- }, [language.id, value, onChange]);
369
- return {
370
- type: "action",
371
- icon: icons.AddIcon,
372
- onAction,
373
- title: language.title,
374
- hidden,
375
- disabled
376
- };
377
- }), AddMissingTranslationsFieldAction = (fieldActionProps, { languages, filteredLanguages }) => {
378
- const value = sanity.useFormValue(fieldActionProps.path), disabled = value && value.length === filteredLanguages.length, hidden = checkAllLanguagesArePresent(filteredLanguages, value), { onChange } = structure.useDocumentPane(), onAction = react.useCallback(() => {
379
- const { schemaType, path } = fieldActionProps, patches = createAddLanguagePatches({
380
- addLanguageKeys: [],
381
- schemaType,
382
- languages,
383
- filteredLanguages,
384
- value,
385
- path
386
- });
387
- onChange(sanity.PatchEvent.from([sanity.setIfMissing([], path), ...patches]));
388
- }, [fieldActionProps, filteredLanguages, languages, onChange, value]);
389
- return {
390
- type: "action",
391
- icon: icons.AddIcon,
392
- onAction,
393
- title: createAddAllTitle(value, filteredLanguages),
394
- disabled,
395
- hidden
396
- };
397
- }, internationalizedArrayFieldAction = sanity.defineDocumentFieldAction({
398
- name: "internationalizedArray",
399
- useAction(fieldActionProps) {
400
- const isInternationalizedArrayField = fieldActionProps?.schemaType?.type?.name.startsWith(
401
- "internationalizedArray"
402
- ), { languages, filteredLanguages } = useInternationalizedArrayContext(), translateFieldActions = createTranslateFieldActions(
403
- fieldActionProps,
404
- { languages, filteredLanguages }
405
- );
406
- return {
407
- type: "group",
408
- icon: icons.TranslateIcon,
409
- title: "Add Translation",
410
- renderAsButton: !0,
411
- children: isInternationalizedArrayField ? [
412
- ...translateFieldActions,
413
- AddMissingTranslationsFieldAction(fieldActionProps, {
414
- languages,
415
- filteredLanguages
416
- })
417
- ] : [],
418
- hidden: !isInternationalizedArrayField
419
- };
420
- }
421
- });
422
- function camelCase(string) {
423
- return string.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
424
- }
425
- function titleCase(string) {
426
- return string.split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
427
- }
428
- function pascalCase(string) {
429
- return titleCase(camelCase(string));
430
- }
431
- function createFieldName(name, addValue = !1) {
432
- return addValue ? ["internationalizedArray", pascalCase(name), "Value"].join("") : ["internationalizedArray", pascalCase(name)].join("");
433
- }
434
- const schemaExample = {
435
- languages: [
436
- { id: "en", title: "English" },
437
- { id: "no", title: "Norsk" }
438
- ]
439
- };
440
- function Feedback() {
441
- return /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "caution", border: !0, radius: 2, padding: 3, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
442
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { children: [
443
- "An array of language objects must be passed into the",
444
- " ",
445
- /* @__PURE__ */ jsxRuntime.jsx("code", { children: "internationalizedArray" }),
446
- " helper function, each with an",
447
- " ",
448
- /* @__PURE__ */ jsxRuntime.jsx("code", { children: "id" }),
449
- " and ",
450
- /* @__PURE__ */ jsxRuntime.jsx("code", { children: "title" }),
451
- " field. Example:"
452
- ] }),
453
- /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 2, border: !0, radius: 2, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Code, { size: 1, language: "javascript", children: JSON.stringify(schemaExample, null, 2) }) })
454
- ] }) });
455
- }
456
- function InternationalizedArray(props) {
457
- const {
458
- members,
459
- value,
460
- schemaType,
461
- onChange,
462
- readOnly: documentReadOnly
463
- } = props, readOnly = typeof schemaType.readOnly == "boolean" ? schemaType.readOnly : !1, toast = ui.useToast(), {
464
- languages,
465
- filteredLanguages,
466
- defaultLanguages,
467
- buttonAddAll,
468
- buttonLocations
469
- } = useInternationalizedArrayContext(), { selectedLanguageIds, options: languageFilterOptions } = languageFilter.useLanguageFilterStudioContext(), documentType = sanity.useFormValue(["_type"]), languageFilterEnabled = typeof documentType == "string" && languageFilterOptions.documentTypes.includes(documentType), filteredMembers = react.useMemo(
470
- () => languageFilterEnabled ? members.filter((member) => {
471
- if (member.kind !== "item")
472
- return !1;
473
- const valueMember = member.item.members[0];
474
- return valueMember.kind !== "field" ? !1 : languageFilterOptions.filterField(
475
- member.item.schemaType,
476
- valueMember,
477
- selectedLanguageIds
478
- );
479
- }) : members,
480
- [languageFilterEnabled, members, languageFilterOptions, selectedLanguageIds]
481
- ), handleAddLanguage = react.useCallback(
482
- async (param) => {
483
- if (!filteredLanguages?.length)
484
- return;
485
- const addLanguageKeys = Array.isArray(param) ? param : [param?.currentTarget?.value].filter(Boolean), patches = createAddLanguagePatches({
486
- addLanguageKeys,
487
- schemaType,
488
- languages,
489
- filteredLanguages,
490
- value
491
- });
492
- onChange([sanity.setIfMissing([]), ...patches]);
493
- },
494
- [filteredLanguages, languages, onChange, schemaType, value]
495
- ), { isDeleting } = structure.useDocumentPane(), addedLanguages = members.map(({ key }) => key), hasAddedDefaultLanguages = defaultLanguages.filter((language) => languages.find((l) => l.id === language)).every((language) => addedLanguages.includes(language));
496
- react.useEffect(() => {
497
- if (!isDeleting && !hasAddedDefaultLanguages) {
498
- const languagesToAdd = defaultLanguages.filter((language) => !addedLanguages.includes(language)).filter((language) => languages.find((l) => l.id === language)), timeout = setTimeout(() => {
499
- documentReadOnly || handleAddLanguage(languagesToAdd);
500
- });
501
- return () => clearTimeout(timeout);
502
- }
503
- }, [
504
- isDeleting,
505
- hasAddedDefaultLanguages,
506
- handleAddLanguage,
507
- defaultLanguages,
508
- addedLanguages,
509
- languages,
510
- documentReadOnly
511
- ]);
512
- const handleRestoreOrder = react.useCallback(() => {
513
- if (!value?.length || !languages?.length)
514
- return;
515
- const updatedValue = value.reduce((acc, v) => {
516
- const newIndex = languages.findIndex((l) => l.id === v?._key);
517
- return newIndex > -1 && (acc[newIndex] = v), acc;
518
- }, []).filter(Boolean);
519
- value?.length !== updatedValue.length && toast.push({
520
- title: "There was an error reordering languages",
521
- status: "warning"
522
- }), onChange(sanity.set(updatedValue));
523
- }, [toast, languages, onChange, value]), allKeysAreLanguages = react.useMemo(() => !value?.length || !languages?.length ? !0 : value?.every((v) => languages.find((l) => l?.id === v?._key)), [value, languages]), languagesInUse = react.useMemo(
524
- () => languages && languages.length > 1 ? languages.filter((l) => value?.find((v) => v._key === l.id)) : [],
525
- [languages, value]
526
- ), languagesOutOfOrder = react.useMemo(() => !value?.length || !languagesInUse.length ? [] : value.map(
527
- (v, vIndex) => vIndex === languagesInUse.findIndex((l) => l.id === v._key) ? null : v
528
- ).filter(Boolean), [value, languagesInUse]), languagesAreValid = react.useMemo(
529
- () => !languages?.length || languages?.length && languages.every((item) => item.id && item.title),
530
- [languages]
531
- );
532
- react.useEffect(() => {
533
- languagesOutOfOrder.length > 0 && allKeysAreLanguages && handleRestoreOrder();
534
- }, [languagesOutOfOrder, allKeysAreLanguages, handleRestoreOrder]);
535
- const allLanguagesArePresent = react.useMemo(
536
- () => checkAllLanguagesArePresent(filteredLanguages, value),
537
- [filteredLanguages, value]
538
- );
539
- if (!languagesAreValid)
540
- return /* @__PURE__ */ jsxRuntime.jsx(Feedback, {});
541
- const addButtonsAreVisible = (
542
- // Plugin was configured to display buttons here (default!)
543
- buttonLocations.includes("field") && // There's at least one language visible
544
- filteredLanguages?.length > 0 && // Not every language has a value yet
545
- !allLanguagesArePresent
546
- ), fieldHasMembers = members?.length > 0;
547
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
548
- fieldHasMembers ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: filteredMembers.map((member) => member.kind === "item" ? /* @__PURE__ */ react.createElement(
549
- sanity.ArrayOfObjectsItem,
550
- {
551
- ...props,
552
- key: member.key,
553
- member
554
- }
555
- ) : /* @__PURE__ */ jsxRuntime.jsx(sanity.MemberItemError, { member }, member.key)) }) : null,
556
- !addButtonsAreVisible && !fieldHasMembers ? /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { border: !0, tone: "transparent", padding: 3, radius: 2, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: "This internationalized field currently has no translations." }) }) : null,
557
- addButtonsAreVisible ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
558
- /* @__PURE__ */ jsxRuntime.jsx(
559
- AddButtons,
560
- {
561
- languages: filteredLanguages,
562
- value,
563
- readOnly,
564
- onClick: handleAddLanguage
565
- }
566
- ),
567
- buttonAddAll ? /* @__PURE__ */ jsxRuntime.jsx(
568
- ui.Button,
569
- {
570
- tone: "primary",
571
- mode: "ghost",
572
- disabled: readOnly || allLanguagesArePresent,
573
- icon: icons.AddIcon,
574
- text: createAddAllTitle(value, filteredLanguages),
575
- onClick: handleAddLanguage
576
- }
577
- ) : null
578
- ] }) : null
579
- ] });
580
- }
581
- function getLanguagesFieldOption(schemaType) {
582
- return schemaType ? schemaType.options?.languages || getLanguagesFieldOption(schemaType.type) : void 0;
583
- }
584
- var array = (config) => {
585
- const { apiVersion, select, languages, type } = config, typeName = typeof type == "string" ? type : type.name, arrayName = createFieldName(typeName), objectName = createFieldName(typeName, !0);
586
- return sanity.defineField({
587
- name: arrayName,
588
- title: "Internationalized array",
589
- type: "array",
590
- components: {
591
- input: InternationalizedArray
592
- },
593
- options: {
594
- // @ts-expect-error - these options are required for validation rules – not the custom input component
595
- apiVersion,
596
- select,
597
- languages
598
- },
599
- of: [
600
- sanity.defineField({
601
- ...typeof type == "string" ? {} : type,
602
- name: objectName,
603
- type: objectName
604
- })
605
- ],
606
- // @ts-expect-error - fix typings
607
- validation: (rule) => rule.custom(async (value, context) => {
608
- if (!value || value.length === 0 || value.length === 1 && !value[0]?._key)
609
- return !0;
610
- const selectedValue = getSelectedValue(select, context.document), client = context.getClient({ apiVersion });
611
- let contextLanguages = [];
612
- const languagesFieldOption = getLanguagesFieldOption(context?.type);
613
- if (Array.isArray(languagesFieldOption))
614
- contextLanguages = languagesFieldOption;
615
- else if (Array.isArray(peek(selectedValue)))
616
- contextLanguages = peek(selectedValue) || [];
617
- else if (typeof languagesFieldOption == "function") {
618
- const cachedLanguages = getFunctionCache(
619
- languagesFieldOption,
620
- selectedValue
621
- );
622
- if (Array.isArray(cachedLanguages))
623
- contextLanguages = cachedLanguages;
624
- else {
625
- const suspendCachedLanguages = peek(selectedValue);
626
- Array.isArray(suspendCachedLanguages) ? contextLanguages = suspendCachedLanguages : (contextLanguages = await languagesFieldOption(
627
- client,
628
- selectedValue
629
- ), setFunctionCache(
630
- languagesFieldOption,
631
- selectedValue,
632
- contextLanguages
633
- ));
634
- }
635
- }
636
- if (value && value.length > contextLanguages.length)
637
- return `Cannot be more than ${contextLanguages.length === 1 ? "1 item" : `${contextLanguages.length} items`}`;
638
- const languageIds = new Set(contextLanguages.map((lang) => lang.id)), nonLanguageKeys = value.filter(
639
- (item) => item?._key && !languageIds.has(item._key)
640
- );
641
- if (nonLanguageKeys.length)
642
- return {
643
- message: "Array item keys must be valid languages registered to the field type",
644
- paths: nonLanguageKeys.map((item) => [{ _key: item._key }])
645
- };
646
- const seenKeys = /* @__PURE__ */ new Set(), duplicateValues = [];
647
- for (const item of value)
648
- item?._key && (seenKeys.has(item._key) ? duplicateValues.push(item) : seenKeys.add(item._key));
649
- return duplicateValues.length ? {
650
- message: "There can only be one field per language",
651
- paths: duplicateValues.map((item) => [{ _key: item._key }])
652
- } : !0;
653
- })
654
- });
655
- };
656
- function getToneFromValidation(validations) {
657
- if (!validations?.length)
658
- return;
659
- const validationLevels = validations.map((v) => v.level);
660
- if (validationLevels.includes("error"))
661
- return "critical";
662
- if (validationLevels.includes("warning"))
663
- return "caution";
664
- }
665
- function InternationalizedInput(props) {
666
- const parentValue = sanity.useFormValue(
667
- props.path.slice(0, -1)
668
- ), originalOnChange = props.inputProps.onChange, wrappedOnChange = react.useCallback(
669
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
670
- (patches) => {
671
- if (!Array.isArray(patches))
672
- return originalOnChange(patches);
673
- const valueField = props.value?.value;
674
- if ((valueField == null || Array.isArray(valueField) && valueField.length === 0) && patches.some((patch) => !patch || typeof patch != "object" ? !1 : patch.type === "insert" && patch.path && Array.isArray(patch.path) && patch.path.length > 0 ? patch.path[0] === "value" || typeof patch.path[0] == "number" : !1)) {
675
- const initPatch = valueField === void 0 ? { type: "setIfMissing", path: ["value"], value: [] } : null, fixedPatches = patches.map((patch) => {
676
- if (!patch || typeof patch != "object")
677
- return patch;
678
- if (patch.type === "insert" && patch.path && Array.isArray(patch.path)) {
679
- const fixedPath = patch.path[0] === "value" ? patch.path : ["value", ...patch.path];
680
- return { ...patch, path: fixedPath };
681
- }
682
- return patch;
683
- }), allPatches = initPatch ? [initPatch, ...fixedPatches] : fixedPatches;
684
- return originalOnChange(allPatches);
685
- }
686
- return originalOnChange(patches);
687
- },
688
- [props.value, originalOnChange]
689
- ), inlineProps = {
690
- ...props.inputProps,
691
- // This is the magic that makes inline editing work?
692
- members: props.inputProps.members.filter(
693
- (m) => m.kind === "field" && m.name === "value"
694
- ),
695
- // This just overrides the type
696
- // Remove this as it shouldn't be necessary?
697
- value: props.value,
698
- // Use our wrapped onChange handler
699
- onChange: wrappedOnChange
700
- }, { validation, value, onChange, readOnly } = inlineProps, { languages, languageDisplay, defaultLanguages } = useInternationalizedArrayContext(), languageKeysInUse = react.useMemo(
701
- () => parentValue?.map((v) => v._key) ?? [],
702
- [parentValue]
703
- ), keyIsValid = languages?.length ? languages.find((l) => l.id === value._key) : !1, handleKeyChange = react.useCallback(
704
- (event) => {
705
- const languageId = event?.currentTarget?.value;
706
- !value || !languages?.length || !languages.find((l) => l.id === languageId) || onChange([sanity.set(languageId, ["_key"])]);
707
- },
708
- [onChange, value, languages]
709
- ), handleUnset = react.useCallback(() => {
710
- onChange(sanity.unset());
711
- }, [onChange]);
712
- if (!languages)
713
- return /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, {});
714
- const language = languages.find((l) => l.id === value._key), languageTitle = keyIsValid && language ? getLanguageDisplay(languageDisplay, language.title, language.id) : "", isDefault = defaultLanguages.includes(value._key), removeButton = /* @__PURE__ */ jsxRuntime.jsx(
715
- ui.Button,
716
- {
717
- mode: "bleed",
718
- icon: icons.RemoveCircleIcon,
719
- tone: "critical",
720
- disabled: readOnly || isDefault,
721
- onClick: handleUnset
722
- }
723
- );
724
- return /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { paddingTop: 2, tone: getToneFromValidation(validation), children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
725
- /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "inherit", children: keyIsValid ? /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { muted: !0, size: 1, children: languageTitle }) : /* @__PURE__ */ jsxRuntime.jsx(
726
- ui.MenuButton,
727
- {
728
- button: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { fontSize: 1, text: `Change "${value._key}"` }),
729
- id: `${value._key}-change-key`,
730
- menu: /* @__PURE__ */ jsxRuntime.jsx(ui.Menu, { children: languages.map((lang) => /* @__PURE__ */ jsxRuntime.jsx(
731
- ui.MenuItem,
732
- {
733
- disabled: languageKeysInUse.includes(lang.id),
734
- fontSize: 1,
735
- text: lang.id.toLocaleUpperCase(),
736
- value: lang.id,
737
- onClick: handleKeyChange
738
- },
739
- lang.id
740
- )) }),
741
- popover: { portal: !0 }
742
- }
743
- ) }),
744
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
745
- /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { flex: 1, tone: "inherit", children: props.inputProps.renderInput(inlineProps) }),
746
- /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "inherit", children: isDefault ? /* @__PURE__ */ jsxRuntime.jsx(
747
- ui.Tooltip,
748
- {
749
- content: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, size: 1, children: "Can't remove default language" }),
750
- fallbackPlacements: ["right", "left"],
751
- placement: "top",
752
- portal: !0,
753
- children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: removeButton })
754
- }
755
- ) : removeButton })
756
- ] })
757
- ] }) });
758
- }
759
- var object = (config) => {
760
- const { type } = config, typeName = typeof type == "string" ? type : type.name, objectName = createFieldName(typeName, !0);
761
- return sanity.defineField({
762
- name: objectName,
763
- title: `Internationalized array ${type}`,
764
- type: "object",
765
- components: {
766
- // @ts-expect-error - fix typings
767
- item: InternationalizedInput
768
- },
769
- fields: [
770
- sanity.defineField({
771
- ...typeof type == "string" ? { type } : type,
772
- name: "value"
773
- })
774
- ],
775
- preview: {
776
- select: {
777
- title: "value",
778
- subtitle: "_key"
779
- }
780
- }
781
- });
782
- };
783
- function flattenSchemaType(schemaType) {
784
- return sanity.isDocumentSchemaType(schemaType) ? extractInnerFields(schemaType.fields, [], 3) : (console.error("Schema type is not a document"), []);
785
- }
786
- function extractInnerFields(fields, path, maxDepth) {
787
- return path.length >= maxDepth ? [] : fields.reduce((acc, field) => {
788
- const thisFieldWithPath = { path: [...path, field.name], ...field };
789
- if (field.type.jsonType === "object") {
790
- const innerFields = extractInnerFields(
791
- field.type.fields,
792
- [...path, field.name],
793
- maxDepth
794
- );
795
- return [...acc, thisFieldWithPath, ...innerFields];
796
- } else if (field.type.jsonType === "array" && field.type.of.length && field.type.of.some((item) => "fields" in item)) {
797
- const innerFields = field.type.of.flatMap(
798
- (innerField) => extractInnerFields(
799
- // @ts-expect-error - Fix TS assertion for array fields
800
- innerField.fields,
801
- [...path, field.name],
802
- maxDepth
803
- )
804
- );
805
- return [...acc, thisFieldWithPath, ...innerFields];
806
- }
807
- return [...acc, thisFieldWithPath];
808
- }, []);
809
- }
810
- const internationalizedArray = sanity.definePlugin((config) => {
811
- const pluginConfig = { ...CONFIG_DEFAULT, ...config }, {
812
- apiVersion = "2025-10-15",
813
- select,
814
- languages,
815
- fieldTypes,
816
- buttonLocations
817
- } = pluginConfig;
818
- return {
819
- name: "sanity-plugin-internationalized-array",
820
- // Preload languages for use throughout the Studio
821
- studio: Array.isArray(languages) ? void 0 : {
822
- components: {
823
- layout: (props) => /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
824
- /* @__PURE__ */ jsxRuntime.jsx(Preload, { apiVersion, languages }),
825
- props.renderDefault(props)
826
- ] })
827
- }
828
- },
829
- // Optional: render "add language" buttons as field actions
830
- document: {
831
- unstable_fieldActions: buttonLocations.includes("unstable__fieldAction") ? (prev) => [...prev, internationalizedArrayFieldAction] : void 0
832
- },
833
- // Wrap document editor with a language provider
834
- form: {
835
- components: {
836
- field: (props) => /* @__PURE__ */ jsxRuntime.jsx(InternationalizedField, { ...props }),
837
- input: (props) => !(props.id === "root" && sanity.isObjectInputProps(props)) || !flattenSchemaType(props.schemaType).map(
838
- (field) => field.type.name
839
- ).some(
840
- (name) => name.startsWith("internationalizedArray")
841
- ) ? props.renderDefault(props) : /* @__PURE__ */ jsxRuntime.jsx(
842
- InternationalizedArrayProvider,
843
- {
844
- ...props,
845
- internationalizedArray: pluginConfig
846
- }
847
- )
848
- }
849
- },
850
- // Register custom schema types for the outer array and the inner object
851
- schema: {
852
- types: [
853
- ...fieldTypes.map(
854
- (type) => array({ type, apiVersion, select, languages })
855
- ),
856
- ...fieldTypes.map((type) => object({ type }))
857
- ]
858
- }
859
- };
860
- });
861
- exports.clear = clear;
862
- exports.internationalizedArray = internationalizedArray;
863
- //# sourceMappingURL=index.js.map