sanity-plugin-internationalized-array 1.10.9 → 2.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.
package/lib/index.mjs ADDED
@@ -0,0 +1,657 @@
1
+ import * as suspend from "suspend-react";
2
+ import { suspend as suspend$1 } from "suspend-react";
3
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
4
+ import { isSanityDocument, setIfMissing, insert, PatchEvent, useClient, useWorkspace, useFormBuilder, defineDocumentFieldAction, useFormValue, set, ArrayOfObjectsItem, MemberItemError, defineField, unset, isDocumentSchemaType, definePlugin, isObjectInputProps } from "sanity";
5
+ import { useLanguageFilterStudioContext } from "@sanity/language-filter";
6
+ import { Grid, Button, useToast, Stack, Box, Text, Card, Code, Label, MenuButton, Menu, MenuItem, Flex, Spinner } from "@sanity/ui";
7
+ import equal from "fast-deep-equal";
8
+ import { useMemo, useCallback, createContext, useContext, useDeferredValue, memo, useRef, useEffect, createElement } from "react";
9
+ import { useDocumentPane } from "sanity/structure";
10
+ import { AddIcon, TranslateIcon, RemoveCircleIcon } from "@sanity/icons";
11
+ import get from "lodash/get.js";
12
+ const namespace = "sanity-plugin-internationalized-array", version = "v0", preload = (fn) => suspend.preload(() => fn(), [version, namespace]), clear = () => suspend.clear([version, namespace]), peek = (selectedValue) => suspend.peek([version, namespace, selectedValue]), MAX_COLUMNS = 7, CONFIG_DEFAULT = {
13
+ languages: [],
14
+ select: {},
15
+ defaultLanguages: [],
16
+ fieldTypes: [],
17
+ apiVersion: "2022-11-27",
18
+ buttonLocations: ["field"],
19
+ buttonAddAll: !0
20
+ };
21
+ function createValueSchemaTypeName(schemaType) {
22
+ return `${schemaType.name}Value`;
23
+ }
24
+ function AddButtons(props) {
25
+ const { languages, readOnly, value, onClick } = props;
26
+ return languages.length > 0 ? /* @__PURE__ */ jsx(Grid, { columns: Math.min(languages.length, MAX_COLUMNS), gap: 2, children: languages.map((language) => /* @__PURE__ */ jsx(
27
+ Button,
28
+ {
29
+ tone: "primary",
30
+ mode: "ghost",
31
+ fontSize: 1,
32
+ disabled: readOnly || !!(value != null && value.find((item) => item._key === language.id)),
33
+ text: language.id.toUpperCase(),
34
+ icon: languages.length > MAX_COLUMNS ? void 0 : AddIcon,
35
+ value: language.id,
36
+ onClick
37
+ },
38
+ language.id
39
+ )) }) : null;
40
+ }
41
+ function DocumentAddButtons(props) {
42
+ const { filteredLanguages } = useInternationalizedArrayContext(), { fields } = props.schemaType, value = isSanityDocument(props.value) ? props.value : void 0, toast = useToast(), { onChange } = useDocumentPane(), internationalizedArrayFields = useMemo(
43
+ () => fields.filter(
44
+ (field) => field.type.name.startsWith("internationalizedArray")
45
+ ),
46
+ [fields]
47
+ ), handleDocumentButtonClick = useCallback(
48
+ (event) => {
49
+ const languageId = event.currentTarget.value;
50
+ if (!languageId) {
51
+ toast.push({
52
+ status: "error",
53
+ title: "No language selected"
54
+ });
55
+ return;
56
+ }
57
+ if (internationalizedArrayFields.length === 0) {
58
+ toast.push({
59
+ status: "error",
60
+ title: "No internationalizedArray fields found in document root"
61
+ });
62
+ return;
63
+ }
64
+ const patches = internationalizedArrayFields.filter(
65
+ (field) => {
66
+ const fieldValue = value == null ? void 0 : value[field.name];
67
+ return !(fieldValue && Array.isArray(fieldValue) && fieldValue.find((v) => v._key === languageId));
68
+ }
69
+ ).map((field) => {
70
+ const fieldKey = field.name;
71
+ return [
72
+ setIfMissing([], [fieldKey]),
73
+ insert(
74
+ [
75
+ {
76
+ _key: languageId,
77
+ _type: createValueSchemaTypeName(field.type)
78
+ }
79
+ ],
80
+ "after",
81
+ [fieldKey, -1]
82
+ )
83
+ ];
84
+ }).flat();
85
+ onChange(PatchEvent.from(patches));
86
+ },
87
+ [internationalizedArrayFields, onChange, toast, value]
88
+ );
89
+ return /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
90
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: "Add translation to internationalized fields" }) }),
91
+ /* @__PURE__ */ jsx(
92
+ AddButtons,
93
+ {
94
+ languages: filteredLanguages,
95
+ readOnly: !1,
96
+ value: void 0,
97
+ onClick: handleDocumentButtonClick
98
+ }
99
+ )
100
+ ] });
101
+ }
102
+ const getSelectedValue = (select, document) => {
103
+ if (!select || !document)
104
+ return {};
105
+ const selection = select || {}, selectedValue = {};
106
+ for (const [key, path] of Object.entries(selection)) {
107
+ let value = get(document, path);
108
+ Array.isArray(value) && (value = value.filter(
109
+ (item) => typeof item == "object" ? (item == null ? void 0 : item._type) === "reference" && "_ref" in item : !0
110
+ )), selectedValue[key] = value;
111
+ }
112
+ return selectedValue;
113
+ }, InternationalizedArrayContext = createContext({
114
+ ...CONFIG_DEFAULT,
115
+ languages: [],
116
+ filteredLanguages: []
117
+ });
118
+ function useInternationalizedArrayContext() {
119
+ return useContext(InternationalizedArrayContext);
120
+ }
121
+ function InternationalizedArrayProvider(props) {
122
+ const { internationalizedArray: internationalizedArray2 } = props, client = useClient({ apiVersion: internationalizedArray2.apiVersion }), workspace = useWorkspace(), { value: document } = useFormBuilder(), deferredDocument = useDeferredValue(document), selectedValue = useMemo(
123
+ () => getSelectedValue(internationalizedArray2.select, deferredDocument),
124
+ [internationalizedArray2.select, deferredDocument]
125
+ ), languages = Array.isArray(internationalizedArray2.languages) ? internationalizedArray2.languages : suspend$1(
126
+ // eslint-disable-next-line require-await
127
+ async () => typeof internationalizedArray2.languages == "function" ? internationalizedArray2.languages(client, selectedValue) : internationalizedArray2.languages,
128
+ [version, namespace, selectedValue, workspace],
129
+ { equal }
130
+ ), { selectedLanguageIds, options: languageFilterOptions } = useLanguageFilterStudioContext(), filteredLanguages = useMemo(() => {
131
+ const documentType = deferredDocument ? deferredDocument._type : void 0;
132
+ return typeof documentType == "string" && languageFilterOptions.documentTypes.includes(documentType) ? languages.filter(
133
+ (language) => selectedLanguageIds.includes(language.id)
134
+ ) : languages;
135
+ }, [deferredDocument, languageFilterOptions, languages, selectedLanguageIds]), showDocumentButtons = internationalizedArray2.buttonLocations.includes("document");
136
+ return /* @__PURE__ */ jsx(
137
+ InternationalizedArrayContext.Provider,
138
+ {
139
+ value: {
140
+ ...internationalizedArray2,
141
+ languages,
142
+ filteredLanguages
143
+ },
144
+ children: showDocumentButtons ? /* @__PURE__ */ jsxs(Stack, { space: 5, children: [
145
+ /* @__PURE__ */ jsx(
146
+ DocumentAddButtons,
147
+ {
148
+ schemaType: props.schemaType,
149
+ value: props.value
150
+ }
151
+ ),
152
+ props.renderDefault(props)
153
+ ] }) : props.renderDefault(props)
154
+ }
155
+ );
156
+ }
157
+ var Preload = memo(function(props) {
158
+ const client = useClient({ apiVersion: props.apiVersion });
159
+ return Array.isArray(peek({})) || preload(
160
+ async () => Array.isArray(props.languages) ? props.languages : props.languages(client, {})
161
+ ), null;
162
+ });
163
+ function checkAllLanguagesArePresent(languages, value) {
164
+ const filteredLanguageIds = languages.map((l) => l.id), languagesInUseIds = value ? value.map((v) => v._key) : [];
165
+ return languagesInUseIds.length === filteredLanguageIds.length && languagesInUseIds.every((l) => filteredLanguageIds.includes(l));
166
+ }
167
+ function createAddAllTitle(value, languages) {
168
+ return value != null && value.length ? `Add missing ${languages.length - value.length === 1 ? "language" : "languages"}` : languages.length === 1 ? `Add ${languages[0].title} Field` : "Add all languages";
169
+ }
170
+ function createAddLanguagePatches(config) {
171
+ const {
172
+ addLanguageKeys,
173
+ schemaType,
174
+ languages,
175
+ filteredLanguages,
176
+ value,
177
+ path = []
178
+ } = config, itemBase = { _type: createValueSchemaTypeName(schemaType) }, newItems = Array.isArray(addLanguageKeys) && addLanguageKeys.length > 0 ? (
179
+ // Just one for this language
180
+ addLanguageKeys.map((id) => ({ ...itemBase, _key: id }))
181
+ ) : (
182
+ // Or one for every missing language
183
+ filteredLanguages.filter(
184
+ (language) => value != null && value.length ? !value.find((v) => v._key === language.id) : !0
185
+ ).map((language) => ({ ...itemBase, _key: language.id }))
186
+ ), languagesInUse = value != null && value.length ? value.map((v) => v) : [];
187
+ return newItems.map((item) => {
188
+ const languageIndex = languages.findIndex((l) => item._key === l.id), remainingLanguages = languages.slice(languageIndex + 1), nextLanguageIndex = languagesInUse.findIndex(
189
+ (l) => (
190
+ // eslint-disable-next-line max-nested-callbacks
191
+ remainingLanguages.find((r) => r.id === l._key)
192
+ )
193
+ );
194
+ return nextLanguageIndex < 0 ? languagesInUse.push(item) : languagesInUse.splice(nextLanguageIndex, 0, item), nextLanguageIndex < 0 ? (
195
+ // No next language (-1), add to end of array
196
+ insert([item], "after", [...path, nextLanguageIndex])
197
+ ) : (
198
+ // Next language found, insert before that
199
+ insert([item], "before", [...path, nextLanguageIndex])
200
+ );
201
+ });
202
+ }
203
+ const createTranslateFieldActions = (fieldActionProps, { languages, filteredLanguages }) => languages.map((language) => {
204
+ const value = useFormValue(fieldActionProps.path), disabled = value && Array.isArray(value) ? !!(value != null && value.find((item) => item._key === language.id)) : !1, hidden = !filteredLanguages.some((f) => f.id === language.id), { onChange } = useDocumentPane(), onAction = useCallback(() => {
205
+ const { schemaType, path } = fieldActionProps, addLanguageKeys = [language.id], patches = createAddLanguagePatches({
206
+ addLanguageKeys,
207
+ schemaType,
208
+ languages,
209
+ filteredLanguages,
210
+ value,
211
+ path
212
+ });
213
+ onChange(PatchEvent.from([setIfMissing([], path), ...patches]));
214
+ }, [language.id, value, onChange]);
215
+ return {
216
+ type: "action",
217
+ icon: AddIcon,
218
+ onAction,
219
+ title: language.title,
220
+ hidden,
221
+ disabled
222
+ };
223
+ }), AddMissingTranslationsFieldAction = (fieldActionProps, { languages, filteredLanguages }) => {
224
+ const value = useFormValue(fieldActionProps.path), disabled = value && value.length === filteredLanguages.length, hidden = checkAllLanguagesArePresent(filteredLanguages, value), { onChange } = useDocumentPane(), onAction = useCallback(() => {
225
+ const { schemaType, path } = fieldActionProps, patches = createAddLanguagePatches({
226
+ addLanguageKeys: [],
227
+ schemaType,
228
+ languages,
229
+ filteredLanguages,
230
+ value,
231
+ path
232
+ });
233
+ onChange(PatchEvent.from([setIfMissing([], path), ...patches]));
234
+ }, [fieldActionProps, filteredLanguages, languages, onChange, value]);
235
+ return {
236
+ type: "action",
237
+ icon: AddIcon,
238
+ onAction,
239
+ title: createAddAllTitle(value, filteredLanguages),
240
+ disabled,
241
+ hidden
242
+ };
243
+ }, internationalizedArrayFieldAction = defineDocumentFieldAction({
244
+ name: "internationalizedArray",
245
+ useAction(fieldActionProps) {
246
+ var _a, _b;
247
+ const isInternationalizedArrayField = (_b = (_a = fieldActionProps == null ? void 0 : fieldActionProps.schemaType) == null ? void 0 : _a.type) == null ? void 0 : _b.name.startsWith(
248
+ "internationalizedArray"
249
+ ), { languages, filteredLanguages } = useInternationalizedArrayContext(), translateFieldActions = createTranslateFieldActions(
250
+ fieldActionProps,
251
+ { languages, filteredLanguages }
252
+ );
253
+ return {
254
+ type: "group",
255
+ icon: TranslateIcon,
256
+ title: "Add Translation",
257
+ renderAsButton: !0,
258
+ children: isInternationalizedArrayField ? [
259
+ ...translateFieldActions,
260
+ AddMissingTranslationsFieldAction(fieldActionProps, {
261
+ languages,
262
+ filteredLanguages
263
+ })
264
+ ] : [],
265
+ hidden: !isInternationalizedArrayField
266
+ };
267
+ }
268
+ });
269
+ function camelCase(string) {
270
+ return string.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
271
+ }
272
+ function titleCase(string) {
273
+ return string.split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
274
+ }
275
+ function pascalCase(string) {
276
+ return titleCase(camelCase(string));
277
+ }
278
+ function createFieldName(name, addValue = !1) {
279
+ return addValue ? ["internationalizedArray", pascalCase(name), "Value"].join("") : ["internationalizedArray", pascalCase(name)].join("");
280
+ }
281
+ const schemaExample = {
282
+ languages: [
283
+ { id: "en", title: "English" },
284
+ { id: "no", title: "Norsk" }
285
+ ]
286
+ };
287
+ function Feedback() {
288
+ return /* @__PURE__ */ jsx(Card, { tone: "caution", border: !0, radius: 2, padding: 3, children: /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
289
+ /* @__PURE__ */ jsxs(Text, { children: [
290
+ "An array of language objects must be passed into the",
291
+ " ",
292
+ /* @__PURE__ */ jsx("code", { children: "internationalizedArray" }),
293
+ " helper function, each with an",
294
+ " ",
295
+ /* @__PURE__ */ jsx("code", { children: "id" }),
296
+ " and ",
297
+ /* @__PURE__ */ jsx("code", { children: "title" }),
298
+ " field. Example:"
299
+ ] }),
300
+ /* @__PURE__ */ jsx(Card, { padding: 2, border: !0, radius: 2, children: /* @__PURE__ */ jsx(Code, { size: 1, language: "javascript", children: JSON.stringify(schemaExample, null, 2) }) })
301
+ ] }) });
302
+ }
303
+ function InternationalizedArray(props) {
304
+ const { members, value, schemaType, onChange } = props, readOnly = typeof schemaType.readOnly == "boolean" ? schemaType.readOnly : !1, toast = useToast(), {
305
+ languages,
306
+ filteredLanguages,
307
+ defaultLanguages,
308
+ buttonAddAll,
309
+ buttonLocations
310
+ } = useInternationalizedArrayContext(), { selectedLanguageIds, options: languageFilterOptions } = useLanguageFilterStudioContext(), documentType = useFormValue(["_type"]), languageFilterEnabled = typeof documentType == "string" && languageFilterOptions.documentTypes.includes(documentType), filteredMembers = useMemo(
311
+ () => languageFilterEnabled ? members.filter((member) => {
312
+ if (member.kind !== "item")
313
+ return !1;
314
+ const valueMember = member.item.members[0];
315
+ return valueMember.kind !== "field" ? !1 : languageFilterOptions.filterField(
316
+ member.item.schemaType,
317
+ valueMember,
318
+ selectedLanguageIds
319
+ );
320
+ }) : members,
321
+ [languageFilterEnabled, members, languageFilterOptions, selectedLanguageIds]
322
+ ), handleAddLanguage = useCallback(
323
+ (param) => {
324
+ var _a;
325
+ if (!(filteredLanguages != null && filteredLanguages.length))
326
+ return;
327
+ const addLanguageKeys = Array.isArray(param) ? param : [(_a = param == null ? void 0 : param.currentTarget) == null ? void 0 : _a.value].filter(Boolean), patches = createAddLanguagePatches({
328
+ addLanguageKeys,
329
+ schemaType,
330
+ languages,
331
+ filteredLanguages,
332
+ value
333
+ });
334
+ onChange([setIfMissing([]), ...patches]);
335
+ },
336
+ [filteredLanguages, languages, onChange, schemaType, value]
337
+ ), documentCreatedAt = useFormValue(["_createdAt"]), hasAddedDefaultLanguages = useRef(!!documentCreatedAt);
338
+ useEffect(() => {
339
+ const shouldAddDefaultLanguages = !hasAddedDefaultLanguages.current && !value && !documentCreatedAt && Array.isArray(defaultLanguages) && defaultLanguages.length > 0;
340
+ return shouldAddDefaultLanguages && handleAddLanguage(defaultLanguages), () => {
341
+ shouldAddDefaultLanguages && (hasAddedDefaultLanguages.current = !0);
342
+ };
343
+ }, [defaultLanguages, documentCreatedAt, handleAddLanguage, value]);
344
+ const handleRestoreOrder = useCallback(() => {
345
+ if (!(value != null && value.length) || !(languages != null && languages.length))
346
+ return;
347
+ const updatedValue = value.reduce((acc, v) => {
348
+ const newIndex = languages.findIndex((l) => l.id === (v == null ? void 0 : v._key));
349
+ return newIndex > -1 && (acc[newIndex] = v), acc;
350
+ }, []).filter(Boolean);
351
+ (value == null ? void 0 : value.length) !== updatedValue.length && toast.push({
352
+ title: "There was an error reordering languages",
353
+ status: "warning"
354
+ }), onChange(set(updatedValue));
355
+ }, [toast, languages, onChange, value]), allKeysAreLanguages = useMemo(() => !(value != null && value.length) || !(languages != null && languages.length) ? !0 : value == null ? void 0 : value.every((v) => languages.find((l) => (l == null ? void 0 : l.id) === (v == null ? void 0 : v._key))), [value, languages]), languagesInUse = useMemo(
356
+ () => languages && languages.length > 1 ? languages.filter((l) => value == null ? void 0 : value.find((v) => v._key === l.id)) : [],
357
+ [languages, value]
358
+ ), languagesOutOfOrder = useMemo(() => !(value != null && value.length) || !languagesInUse.length ? [] : value.map(
359
+ (v, vIndex) => vIndex === languagesInUse.findIndex((l) => l.id === v._key) ? null : v
360
+ ).filter(Boolean), [value, languagesInUse]), languagesAreValid = useMemo(
361
+ () => !(languages != null && languages.length) || (languages == null ? void 0 : languages.length) && languages.every((item) => item.id && item.title),
362
+ [languages]
363
+ );
364
+ useEffect(() => {
365
+ languagesOutOfOrder.length > 0 && allKeysAreLanguages && handleRestoreOrder();
366
+ }, [languagesOutOfOrder, allKeysAreLanguages, handleRestoreOrder]);
367
+ const allLanguagesArePresent = useMemo(
368
+ () => checkAllLanguagesArePresent(filteredLanguages, value),
369
+ [filteredLanguages, value]
370
+ );
371
+ if (!languagesAreValid)
372
+ return /* @__PURE__ */ jsx(Feedback, {});
373
+ const addButtonsAreVisible = (
374
+ // Plugin was configured to display buttons here (default!)
375
+ buttonLocations.includes("field") && // There's at least one language visible
376
+ (filteredLanguages == null ? void 0 : filteredLanguages.length) > 0 && // Not every language has a value yet
377
+ !allLanguagesArePresent
378
+ ), fieldHasMembers = (members == null ? void 0 : members.length) > 0;
379
+ return /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
380
+ fieldHasMembers ? /* @__PURE__ */ jsx(Fragment, { children: filteredMembers.map((member) => member.kind === "item" ? /* @__PURE__ */ createElement(
381
+ ArrayOfObjectsItem,
382
+ {
383
+ ...props,
384
+ key: member.key,
385
+ member
386
+ }
387
+ ) : /* @__PURE__ */ jsx(MemberItemError, { member }, member.key)) }) : null,
388
+ !addButtonsAreVisible && !fieldHasMembers ? /* @__PURE__ */ jsx(Card, { border: !0, tone: "transparent", padding: 3, radius: 2, children: /* @__PURE__ */ jsx(Text, { size: 1, children: "This internationalized field currently has no translations." }) }) : null,
389
+ addButtonsAreVisible ? /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
390
+ /* @__PURE__ */ jsx(
391
+ AddButtons,
392
+ {
393
+ languages: filteredLanguages,
394
+ value,
395
+ readOnly,
396
+ onClick: handleAddLanguage
397
+ }
398
+ ),
399
+ buttonAddAll ? /* @__PURE__ */ jsx(
400
+ Button,
401
+ {
402
+ tone: "primary",
403
+ mode: "ghost",
404
+ disabled: readOnly || allLanguagesArePresent,
405
+ icon: AddIcon,
406
+ text: createAddAllTitle(value, filteredLanguages),
407
+ onClick: handleAddLanguage
408
+ }
409
+ ) : null
410
+ ] }) : null
411
+ ] });
412
+ }
413
+ function getLanguagesFieldOption(schemaType) {
414
+ var _a;
415
+ return schemaType ? ((_a = schemaType.options) == null ? void 0 : _a.languages) || getLanguagesFieldOption(schemaType.type) : void 0;
416
+ }
417
+ var array = (config) => {
418
+ const { apiVersion, select, languages, type } = config, typeName = typeof type == "string" ? type : type.name, arrayName = createFieldName(typeName), objectName = createFieldName(typeName, !0);
419
+ return defineField({
420
+ name: arrayName,
421
+ title: "Internationalized array",
422
+ type: "array",
423
+ components: {
424
+ input: InternationalizedArray
425
+ },
426
+ // These options are required for validation rules – not the custom input component
427
+ options: { apiVersion, select, languages },
428
+ of: [
429
+ defineField({
430
+ ...typeof type == "string" ? {} : type,
431
+ name: objectName,
432
+ type: objectName
433
+ })
434
+ ],
435
+ validation: (rule) => rule.custom(async (value, context) => {
436
+ if (!value)
437
+ return !0;
438
+ const selectedValue = getSelectedValue(select, context.document), client = context.getClient({ apiVersion });
439
+ let contextLanguages = [];
440
+ const languagesFieldOption = getLanguagesFieldOption(context == null ? void 0 : context.type);
441
+ if (Array.isArray(languagesFieldOption) ? contextLanguages = languagesFieldOption : Array.isArray(peek(selectedValue)) ? contextLanguages = peek(selectedValue) || [] : typeof languagesFieldOption == "function" && (contextLanguages = await languagesFieldOption(client, selectedValue)), value && value.length > contextLanguages.length)
442
+ return `Cannot be more than ${contextLanguages.length === 1 ? "1 item" : `${contextLanguages.length} items`}`;
443
+ const nonLanguageKeys = value != null && value.length ? value.filter(
444
+ (item) => !contextLanguages.find((language) => item._key === language.id)
445
+ ) : [];
446
+ if (nonLanguageKeys.length)
447
+ return {
448
+ message: "Array item keys must be valid languages registered to the field type",
449
+ paths: nonLanguageKeys.map((item) => [{ _key: item._key }])
450
+ };
451
+ const valuesByLanguage = value != null && value.length ? value.filter((item) => !!(item != null && item._key)).reduce((acc, cur) => acc[cur._key] ? { ...acc, [cur._key]: [...acc[cur._key], cur] } : {
452
+ ...acc,
453
+ [cur._key]: [cur]
454
+ }, {}) : {}, duplicateValues = Object.values(valuesByLanguage).filter((item) => (item == null ? void 0 : item.length) > 1).flat();
455
+ return duplicateValues.length ? {
456
+ message: "There can only be one field per language",
457
+ paths: duplicateValues.map((item) => [{ _key: item._key }])
458
+ } : !0;
459
+ })
460
+ });
461
+ };
462
+ function InternationalizedField(props) {
463
+ return props.schemaType.name === "reference" && props.value ? props.renderDefault({
464
+ ...props,
465
+ title: "",
466
+ level: 0
467
+ }) : props.children;
468
+ }
469
+ function getToneFromValidation(validations) {
470
+ if (!(validations != null && validations.length))
471
+ return;
472
+ const validationLevels = validations.map((v) => v.level);
473
+ if (validationLevels.includes("error"))
474
+ return "critical";
475
+ if (validationLevels.includes("warning"))
476
+ return "caution";
477
+ }
478
+ function InternationalizedInput(props) {
479
+ const parentValue = useFormValue(
480
+ props.path.slice(0, -1)
481
+ ), inlineProps = {
482
+ ...props.inputProps,
483
+ // This is the magic that makes inline editing work?
484
+ members: props.inputProps.members.filter(
485
+ (m) => m.kind === "field" && m.name === "value"
486
+ ),
487
+ // This just overrides the type
488
+ // TODO: Remove this as it shouldn't be necessary?
489
+ value: props.value
490
+ }, { validation, value, onChange, readOnly } = inlineProps, { languages } = useInternationalizedArrayContext(), languageKeysInUse = useMemo(
491
+ () => {
492
+ var _a;
493
+ return (_a = parentValue == null ? void 0 : parentValue.map((v) => v._key)) != null ? _a : [];
494
+ },
495
+ [parentValue]
496
+ ), keyIsValid = languages != null && languages.length ? languages.find((l) => l.id === value._key) : !1, handleKeyChange = useCallback(
497
+ (event) => {
498
+ var _a;
499
+ const languageId = (_a = event == null ? void 0 : event.currentTarget) == null ? void 0 : _a.value;
500
+ !value || !(languages != null && languages.length) || !languages.find((l) => l.id === languageId) || onChange([set(languageId, ["_key"])]);
501
+ },
502
+ [onChange, value, languages]
503
+ ), handleUnset = useCallback(() => {
504
+ onChange(unset());
505
+ }, [onChange]);
506
+ return languages ? /* @__PURE__ */ jsx(Card, { paddingTop: 2, tone: getToneFromValidation(validation), children: /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
507
+ /* @__PURE__ */ jsx(Card, { tone: "inherit", children: keyIsValid ? /* @__PURE__ */ jsx(Label, { muted: !0, size: 1, children: value._key }) : /* @__PURE__ */ jsx(
508
+ MenuButton,
509
+ {
510
+ button: /* @__PURE__ */ jsx(Button, { fontSize: 1, text: `Change "${value._key}"` }),
511
+ id: `${value._key}-change-key`,
512
+ menu: /* @__PURE__ */ jsx(Menu, { children: languages.map((language) => /* @__PURE__ */ jsx(
513
+ MenuItem,
514
+ {
515
+ disabled: languageKeysInUse.includes(language.id),
516
+ fontSize: 1,
517
+ text: language.id.toLocaleUpperCase(),
518
+ value: language.id,
519
+ onClick: handleKeyChange
520
+ },
521
+ language.id
522
+ )) }),
523
+ popover: { portal: !0 }
524
+ }
525
+ ) }),
526
+ /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
527
+ /* @__PURE__ */ jsx(Card, { flex: 1, tone: "inherit", children: props.inputProps.renderInput(props.inputProps) }),
528
+ /* @__PURE__ */ jsx(Card, { tone: "inherit", children: /* @__PURE__ */ jsx(
529
+ Button,
530
+ {
531
+ mode: "bleed",
532
+ icon: RemoveCircleIcon,
533
+ tone: "critical",
534
+ disabled: readOnly,
535
+ onClick: handleUnset
536
+ }
537
+ ) })
538
+ ] })
539
+ ] }) }) : /* @__PURE__ */ jsx(Spinner, {});
540
+ }
541
+ var object = (config) => {
542
+ const { type } = config, typeName = typeof type == "string" ? type : type.name, objectName = createFieldName(typeName, !0);
543
+ return defineField({
544
+ name: objectName,
545
+ title: `Internationalized array ${type}`,
546
+ type: "object",
547
+ components: {
548
+ item: InternationalizedInput
549
+ },
550
+ // @ts-expect-error - Address this typing issue with the inner object
551
+ fields: [
552
+ typeof type == "string" ? (
553
+ // Define a simple field if all we have is the name as a string
554
+ defineField({
555
+ name: "value",
556
+ type,
557
+ components: {
558
+ field: InternationalizedField
559
+ }
560
+ })
561
+ ) : (
562
+ // Pass in the configured options, but overwrite the name
563
+ {
564
+ ...type,
565
+ name: "value",
566
+ components: {
567
+ field: InternationalizedField
568
+ }
569
+ }
570
+ )
571
+ ],
572
+ preview: {
573
+ select: {
574
+ title: "value",
575
+ subtitle: "_key"
576
+ }
577
+ }
578
+ });
579
+ };
580
+ function flattenSchemaType(schemaType) {
581
+ return isDocumentSchemaType(schemaType) ? extractInnerFields(schemaType.fields, [], 3) : (console.error("Schema type is not a document"), []);
582
+ }
583
+ function extractInnerFields(fields, path, maxDepth) {
584
+ return path.length >= maxDepth ? [] : fields.reduce((acc, field) => {
585
+ const thisFieldWithPath = { path: [...path, field.name], ...field };
586
+ if (field.type.jsonType === "object") {
587
+ const innerFields = extractInnerFields(
588
+ field.type.fields,
589
+ [...path, field.name],
590
+ maxDepth
591
+ );
592
+ return [...acc, thisFieldWithPath, ...innerFields];
593
+ } else if (field.type.jsonType === "array" && field.type.of.length && field.type.of.some((item) => "fields" in item)) {
594
+ const innerFields = extractInnerFields(
595
+ // @ts-expect-error - Fix TS assertion for array fields
596
+ field.type.of[0].fields,
597
+ [...path, field.name],
598
+ maxDepth
599
+ );
600
+ return [...acc, thisFieldWithPath, ...innerFields];
601
+ }
602
+ return [...acc, thisFieldWithPath];
603
+ }, []);
604
+ }
605
+ const internationalizedArray = definePlugin((config) => {
606
+ const pluginConfig = { ...CONFIG_DEFAULT, ...config }, {
607
+ apiVersion = "2022-11-27",
608
+ select,
609
+ languages,
610
+ fieldTypes,
611
+ defaultLanguages,
612
+ buttonLocations
613
+ } = pluginConfig;
614
+ return {
615
+ name: "sanity-plugin-internationalized-array",
616
+ // Preload languages for use throughout the Studio
617
+ studio: Array.isArray(languages) ? void 0 : {
618
+ components: {
619
+ layout: (props) => /* @__PURE__ */ jsxs(Fragment, { children: [
620
+ /* @__PURE__ */ jsx(Preload, { apiVersion, languages }),
621
+ props.renderDefault(props)
622
+ ] })
623
+ }
624
+ },
625
+ // Optional: render "add language" buttons as field actions
626
+ document: {
627
+ unstable_fieldActions: buttonLocations.includes("unstable__fieldAction") ? (prev) => [...prev, internationalizedArrayFieldAction] : void 0
628
+ },
629
+ // Wrap document editor with a language provider
630
+ form: {
631
+ components: {
632
+ input: (props) => !(props.id === "root" && isObjectInputProps(props)) || !flattenSchemaType(props.schemaType).map(
633
+ (field) => field.type.name
634
+ ).some(
635
+ (name) => name.startsWith("internationalizedArray")
636
+ ) ? props.renderDefault(props) : InternationalizedArrayProvider({
637
+ ...props,
638
+ internationalizedArray: pluginConfig
639
+ })
640
+ }
641
+ },
642
+ // Register custom schema types for the outer array and the inner object
643
+ schema: {
644
+ types: [
645
+ ...fieldTypes.map(
646
+ (type) => array({ type, apiVersion, select, languages, defaultLanguages })
647
+ ),
648
+ ...fieldTypes.map((type) => object({ type }))
649
+ ]
650
+ }
651
+ };
652
+ });
653
+ export {
654
+ clear,
655
+ internationalizedArray
656
+ };
657
+ //# sourceMappingURL=index.mjs.map