sanity-plugin-internationalized-array 3.1.4 → 3.1.6

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.esm.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as suspend from "suspend-react";
2
2
  import { suspend as suspend$1 } from "suspend-react";
3
3
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
4
- import { isSanityDocument, setIfMissing, insert, PatchEvent, useClient, useWorkspace, defineDocumentFieldAction, useFormValue, set, ArrayOfObjectsItem, MemberItemError, defineField, unset, isDocumentSchemaType, definePlugin, isObjectInputProps } from "sanity";
4
+ import { isSanityDocument, useSchema, setIfMissing, insert, PatchEvent, useClient, useWorkspace, defineDocumentFieldAction, useFormValue, set, ArrayOfObjectsItem, MemberItemError, defineField, unset, isDocumentSchemaType, definePlugin, isObjectInputProps } from "sanity";
5
5
  import { useLanguageFilterStudioContext } from "@sanity/language-filter";
6
6
  import { Grid, Button, useToast, Stack, Box, Text, Card, Code, Spinner, Label, MenuButton, Menu, MenuItem, Flex, Tooltip } from "@sanity/ui";
7
7
  import equal from "fast-deep-equal";
@@ -9,7 +9,32 @@ import { memo, useCallback, createContext, useContext, useDeferredValue, useMemo
9
9
  import { useDocumentPane } from "sanity/structure";
10
10
  import { AddIcon, TranslateIcon, RemoveCircleIcon } from "@sanity/icons";
11
11
  import get from "lodash/get.js";
12
- const namespace = "sanity-plugin-internationalized-array", version = "v1", preload = (fn) => suspend.preload(() => fn(), [version, namespace]), clear = () => suspend.clear([version, namespace]), peek = (selectedValue) => suspend.peek([version, namespace, selectedValue]), MAX_COLUMNS = {
12
+ const namespace = "sanity-plugin-internationalized-array", version = "v1", functionCache = /* @__PURE__ */ new Map(), functionKeyCache = /* @__PURE__ */ new WeakMap(), preloadWithKey = (fn, key) => suspend.preload(() => fn(), key), clear = () => suspend.clear([version, namespace]), peek = (selectedValue) => suspend.peek([version, namespace, selectedValue]), createCacheKey = (selectedValue, workspaceId) => {
13
+ const selectedValueHash = JSON.stringify(selectedValue);
14
+ return workspaceId ? [version, namespace, selectedValueHash, workspaceId] : [version, namespace, selectedValueHash];
15
+ }, getFunctionKey = (fn) => {
16
+ const cachedKey = functionKeyCache.get(fn);
17
+ if (cachedKey)
18
+ return cachedKey;
19
+ const fnStr = fn.toString();
20
+ let hash = 0;
21
+ const maxLength = Math.min(fnStr.length, 100);
22
+ for (let i = 0; i < maxLength; i++) {
23
+ const char = fnStr.charCodeAt(i);
24
+ hash = (hash << 5) - hash + char, hash &= hash;
25
+ }
26
+ const key = `anonymous_${Math.abs(hash)}`;
27
+ return functionKeyCache.set(fn, key), key;
28
+ }, createFunctionCacheKey = (fn, selectedValue, workspaceId) => {
29
+ const functionKey = getFunctionKey(fn), selectedValueHash = JSON.stringify(selectedValue);
30
+ return workspaceId ? `${functionKey}:${selectedValueHash}:${workspaceId}` : `${functionKey}:${selectedValueHash}`;
31
+ }, getFunctionCache = (fn, selectedValue, workspaceId) => {
32
+ const key = createFunctionCacheKey(fn, selectedValue, workspaceId);
33
+ return functionCache.get(key);
34
+ }, setFunctionCache = (fn, selectedValue, languages, workspaceId) => {
35
+ const key = createFunctionCacheKey(fn, selectedValue, workspaceId);
36
+ functionCache.set(key, languages);
37
+ }, MAX_COLUMNS = {
13
38
  codeOnly: 5,
14
39
  titleOnly: 4,
15
40
  titleAndCode: 3
@@ -95,7 +120,43 @@ function AddButtons(props) {
95
120
  }
96
121
  var AddButtons$1 = memo(AddButtons);
97
122
  function DocumentAddButtons(props) {
98
- const { filteredLanguages } = useInternationalizedArrayContext(), value = isSanityDocument(props.value) ? props.value : void 0, toast = useToast(), { onChange } = useDocumentPane(), documentsToTranslation = getDocumentsToTranslate(value, []), handleDocumentButtonClick = useCallback(
123
+ const { filteredLanguages } = useInternationalizedArrayContext(), value = isSanityDocument(props.value) ? props.value : void 0, toast = useToast(), { onChange } = useDocumentPane(), schema = useSchema(), documentsToTranslation = getDocumentsToTranslate(value, []), getInitialValueForType = useCallback(
124
+ (typeName) => {
125
+ var _a;
126
+ if (!typeName) return;
127
+ const match = typeName.match(/^internationalizedArray(.+)Value$/);
128
+ if (!match) return;
129
+ const baseTypeName = match[1].charAt(0).toLowerCase() + match[1].slice(1), arrayBasedTypes = [
130
+ "body",
131
+ "htmlContent",
132
+ "blockContent",
133
+ "portableText"
134
+ ];
135
+ if (arrayBasedTypes.includes(baseTypeName))
136
+ return [];
137
+ try {
138
+ const schemaType = schema.get(typeName);
139
+ if (schemaType) {
140
+ const valueField = (_a = schemaType == null ? void 0 : schemaType.fields) == null ? void 0 : _a.find(
141
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
142
+ (f) => f.name === "value"
143
+ );
144
+ if (valueField) {
145
+ const fieldType = valueField.type;
146
+ if ((fieldType == null ? void 0 : fieldType.jsonType) === "array" || (fieldType == null ? void 0 : fieldType.name) === "array" || (fieldType == null ? void 0 : fieldType.type) === "array" || (fieldType == null ? void 0 : fieldType.of) !== void 0 || arrayBasedTypes.includes(fieldType == null ? void 0 : fieldType.name))
147
+ return [];
148
+ }
149
+ }
150
+ } catch (error) {
151
+ console.warn(
152
+ "Could not determine field type from schema:",
153
+ typeName,
154
+ error
155
+ );
156
+ }
157
+ },
158
+ [schema]
159
+ ), handleDocumentButtonClick = useCallback(
99
160
  async (event) => {
100
161
  const languageId = event.currentTarget.value;
101
162
  if (!languageId) {
@@ -121,12 +182,13 @@ function DocumentAddButtons(props) {
121
182
  }
122
183
  const patches = [];
123
184
  for (const toTranslate of removeDuplicates) {
124
- const path = toTranslate.path, ifMissing = setIfMissing([], path), insertValue = insert(
185
+ const path = toTranslate.path, initialValue = getInitialValueForType(toTranslate._type), ifMissing = setIfMissing([], path), insertValue = insert(
125
186
  [
126
187
  {
127
188
  _key: languageId,
128
189
  _type: toTranslate._type,
129
- value: void 0
190
+ value: initialValue
191
+ // Use the determined initial value instead of undefined
130
192
  }
131
193
  ],
132
194
  "after",
@@ -136,7 +198,7 @@ function DocumentAddButtons(props) {
136
198
  }
137
199
  onChange(PatchEvent.from(patches.flat()));
138
200
  },
139
- [documentsToTranslation, onChange, toast]
201
+ [documentsToTranslation, getInitialValueForType, onChange, toast]
140
202
  );
141
203
  return /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
142
204
  /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: "Add translation to internationalized fields" }) }),
@@ -182,10 +244,36 @@ function InternationalizedArrayProvider(props) {
182
244
  const { internationalizedArray: internationalizedArray2 } = props, client = useClient({ apiVersion: internationalizedArray2.apiVersion }), workspace = useWorkspace(), { formState } = useDocumentPane(), deferredDocument = useDeferredValue(formState == null ? void 0 : formState.value), selectedValue = useMemo(
183
245
  () => getSelectedValue(internationalizedArray2.select, deferredDocument),
184
246
  [internationalizedArray2.select, deferredDocument]
247
+ ), workspaceId = useMemo(() => {
248
+ if (workspace != null && workspace.name)
249
+ return workspace.name;
250
+ const workspaceKey = {
251
+ name: workspace == null ? void 0 : workspace.name,
252
+ title: workspace == null ? void 0 : workspace.title
253
+ // Add other stable properties as needed
254
+ };
255
+ return JSON.stringify(workspaceKey);
256
+ }, [workspace]), cacheKey = useMemo(
257
+ () => createCacheKey(selectedValue, workspaceId),
258
+ [selectedValue, workspaceId]
185
259
  ), languages = Array.isArray(internationalizedArray2.languages) ? internationalizedArray2.languages : suspend$1(
186
260
  // eslint-disable-next-line require-await
187
- async () => typeof internationalizedArray2.languages == "function" ? internationalizedArray2.languages(client, selectedValue) : internationalizedArray2.languages,
188
- [version, namespace, selectedValue, workspace],
261
+ async () => {
262
+ if (typeof internationalizedArray2.languages == "function") {
263
+ const result = await internationalizedArray2.languages(
264
+ client,
265
+ selectedValue
266
+ );
267
+ return setFunctionCache(
268
+ internationalizedArray2.languages,
269
+ selectedValue,
270
+ result,
271
+ workspaceId
272
+ ), result;
273
+ }
274
+ return internationalizedArray2.languages;
275
+ },
276
+ cacheKey,
189
277
  { equal }
190
278
  ), { selectedLanguageIds, options: languageFilterOptions } = useLanguageFilterStudioContext(), filteredLanguages = useMemo(() => {
191
279
  const documentType = deferredDocument ? deferredDocument._type : void 0;
@@ -227,10 +315,13 @@ function InternationalizedField(props) {
227
315
  })) : customProps.renderDefault(customProps);
228
316
  }
229
317
  var Preload = memo(function(props) {
230
- const client = useClient({ apiVersion: props.apiVersion });
231
- return Array.isArray(peek({})) || preload(
232
- async () => Array.isArray(props.languages) ? props.languages : props.languages(client, {})
233
- ), null;
318
+ const client = useClient({ apiVersion: props.apiVersion }), cacheKey = createCacheKey({});
319
+ return Array.isArray(peek({})) || preloadWithKey(async () => {
320
+ if (Array.isArray(props.languages))
321
+ return props.languages;
322
+ const result = await props.languages(client, {});
323
+ return setFunctionCache(props.languages, {}, result), result;
324
+ }, cacheKey), null;
234
325
  });
235
326
  function checkAllLanguagesArePresent(languages, value) {
236
327
  const filteredLanguageIds = languages.map((l) => l.id), languagesInUseIds = value ? value.map((v) => v._key) : [];
@@ -539,24 +630,48 @@ var __defProp$4 = Object.defineProperty, __defProps$3 = Object.defineProperties,
539
630
  ],
540
631
  // @ts-expect-error - fix typings
541
632
  validation: (rule) => rule.custom(async (value, context) => {
542
- if (!value)
633
+ var _a;
634
+ if (!value || value.length === 0 || value.length === 1 && !((_a = value[0]) != null && _a._key))
543
635
  return !0;
544
636
  const selectedValue = getSelectedValue(select, context.document), client = context.getClient({ apiVersion });
545
637
  let contextLanguages = [];
546
638
  const languagesFieldOption = getLanguagesFieldOption(context == null ? void 0 : context.type);
547
- 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)
639
+ if (Array.isArray(languagesFieldOption))
640
+ contextLanguages = languagesFieldOption;
641
+ else if (Array.isArray(peek(selectedValue)))
642
+ contextLanguages = peek(selectedValue) || [];
643
+ else if (typeof languagesFieldOption == "function") {
644
+ const cachedLanguages = getFunctionCache(
645
+ languagesFieldOption,
646
+ selectedValue
647
+ );
648
+ if (Array.isArray(cachedLanguages))
649
+ contextLanguages = cachedLanguages;
650
+ else {
651
+ const suspendCachedLanguages = peek(selectedValue);
652
+ Array.isArray(suspendCachedLanguages) ? contextLanguages = suspendCachedLanguages : (contextLanguages = await languagesFieldOption(
653
+ client,
654
+ selectedValue
655
+ ), setFunctionCache(
656
+ languagesFieldOption,
657
+ selectedValue,
658
+ contextLanguages
659
+ ));
660
+ }
661
+ }
662
+ if (value && value.length > contextLanguages.length)
548
663
  return `Cannot be more than ${contextLanguages.length === 1 ? "1 item" : `${contextLanguages.length} items`}`;
549
- const nonLanguageKeys = value != null && value.length ? value.filter(
550
- (item) => !contextLanguages.find((language) => item._key === language.id)
551
- ) : [];
664
+ const languageIds = new Set(contextLanguages.map((lang) => lang.id)), nonLanguageKeys = value.filter(
665
+ (item) => (item == null ? void 0 : item._key) && !languageIds.has(item._key)
666
+ );
552
667
  if (nonLanguageKeys.length)
553
668
  return {
554
669
  message: "Array item keys must be valid languages registered to the field type",
555
670
  paths: nonLanguageKeys.map((item) => [{ _key: item._key }])
556
671
  };
557
- const valuesByLanguage = value != null && value.length ? value.filter((item) => !!(item != null && item._key)).reduce((acc, cur) => acc[cur._key] ? __spreadProps$3(__spreadValues$4({}, acc), { [cur._key]: [...acc[cur._key], cur] }) : __spreadProps$3(__spreadValues$4({}, acc), {
558
- [cur._key]: [cur]
559
- }), {}) : {}, duplicateValues = Object.values(valuesByLanguage).filter((item) => (item == null ? void 0 : item.length) > 1).flat();
672
+ const seenKeys = /* @__PURE__ */ new Set(), duplicateValues = [];
673
+ for (const item of value)
674
+ item != null && item._key && (seenKeys.has(item._key) ? duplicateValues.push(item) : seenKeys.add(item._key));
560
675
  return duplicateValues.length ? {
561
676
  message: "There can only be one field per language",
562
677
  paths: duplicateValues.map((item) => [{ _key: item._key }])
@@ -584,6 +699,28 @@ var __defProp$3 = Object.defineProperty, __defProps$2 = Object.defineProperties,
584
699
  function InternationalizedInput(props) {
585
700
  const parentValue = useFormValue(
586
701
  props.path.slice(0, -1)
702
+ ), originalOnChange = props.inputProps.onChange, wrappedOnChange = useCallback(
703
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
704
+ (patches) => {
705
+ var _a;
706
+ if (!Array.isArray(patches))
707
+ return originalOnChange(patches);
708
+ const valueField = (_a = props.value) == null ? void 0 : _a.value;
709
+ 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)) {
710
+ const initPatch = valueField === void 0 ? { type: "setIfMissing", path: ["value"], value: [] } : null, fixedPatches = patches.map((patch) => {
711
+ if (!patch || typeof patch != "object")
712
+ return patch;
713
+ if (patch.type === "insert" && patch.path && Array.isArray(patch.path)) {
714
+ const fixedPath = patch.path[0] === "value" ? patch.path : ["value", ...patch.path];
715
+ return __spreadProps$2(__spreadValues$3({}, patch), { path: fixedPath });
716
+ }
717
+ return patch;
718
+ }), allPatches = initPatch ? [initPatch, ...fixedPatches] : fixedPatches;
719
+ return originalOnChange(allPatches);
720
+ }
721
+ return originalOnChange(patches);
722
+ },
723
+ [props.value, originalOnChange]
587
724
  ), inlineProps = __spreadProps$2(__spreadValues$3({}, props.inputProps), {
588
725
  // This is the magic that makes inline editing work?
589
726
  members: props.inputProps.members.filter(
@@ -591,7 +728,9 @@ function InternationalizedInput(props) {
591
728
  ),
592
729
  // This just overrides the type
593
730
  // Remove this as it shouldn't be necessary?
594
- value: props.value
731
+ value: props.value,
732
+ // Use our wrapped onChange handler
733
+ onChange: wrappedOnChange
595
734
  }), { validation, value, onChange, readOnly } = inlineProps, { languages, languageDisplay, defaultLanguages } = useInternationalizedArrayContext(), languageKeysInUse = useMemo(
596
735
  () => {
597
736
  var _a;
@@ -641,7 +780,7 @@ function InternationalizedInput(props) {
641
780
  }
642
781
  ) }),
643
782
  /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
644
- /* @__PURE__ */ jsx(Card, { flex: 1, tone: "inherit", children: props.inputProps.renderInput(props.inputProps) }),
783
+ /* @__PURE__ */ jsx(Card, { flex: 1, tone: "inherit", children: props.inputProps.renderInput(inlineProps) }),
645
784
  /* @__PURE__ */ jsx(Card, { tone: "inherit", children: isDefault ? /* @__PURE__ */ jsx(
646
785
  Tooltip,
647
786
  {