sanity-plugin-media 4.1.0 → 4.2.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 (62) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +56 -4
  3. package/dist/index.d.mts +131 -57
  4. package/dist/index.d.ts +131 -57
  5. package/dist/index.js +273 -106
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +273 -106
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +11 -4
  10. package/src/__tests__/fixtures/createEpicTestStore.ts +27 -0
  11. package/src/__tests__/fixtures/listenMock.ts +9 -0
  12. package/src/__tests__/fixtures/mockSanityClient.ts +84 -0
  13. package/src/__tests__/fixtures/renderWithProviders.tsx +54 -0
  14. package/src/__tests__/fixtures/rootState.ts +27 -0
  15. package/src/__tests__/fixtures/withinDialog.ts +28 -0
  16. package/src/components/Browser/Browser.test.tsx +44 -0
  17. package/src/components/CardAsset/CardAsset.test.tsx +322 -0
  18. package/src/components/DialogAssetEdit/Details.tsx +123 -44
  19. package/src/components/DialogAssetEdit/DialogAssetEdit.test.tsx +215 -0
  20. package/src/components/DialogAssetEdit/index.tsx +138 -30
  21. package/src/components/DialogTagCreate/DialogTagCreate.test.tsx +120 -0
  22. package/src/components/DialogTagEdit/DialogTagEdit.test.tsx +164 -0
  23. package/src/components/FormBuilderTool/FormBuilderTool.test.tsx +62 -0
  24. package/src/components/ReduxProvider/index.tsx +2 -1
  25. package/src/components/UploadDropzone/UploadDropzone.test.tsx +39 -0
  26. package/src/constants.ts +6 -0
  27. package/src/contexts/ToolOptionsContext.tsx +6 -3
  28. package/src/formSchema/index.test.ts +55 -0
  29. package/src/formSchema/index.ts +28 -12
  30. package/src/hooks/useVersionedClient.ts +1 -1
  31. package/src/modules/assets/deleteAndUpdateEpics.test.ts +86 -0
  32. package/src/modules/assets/fetchEpic.test.ts +72 -0
  33. package/src/modules/assets/reducer.test.ts +90 -0
  34. package/src/modules/assets/tagsAndListenerEpics.test.ts +205 -0
  35. package/src/modules/dialog/epics.test.ts +167 -0
  36. package/src/modules/dialog/reducer.test.ts +184 -0
  37. package/src/modules/notifications/epics.test.ts +373 -0
  38. package/src/modules/notifications/index.ts +24 -4
  39. package/src/modules/notifications/reducer.test.ts +53 -0
  40. package/src/modules/search/index.test.ts +35 -0
  41. package/src/modules/selectors.test.ts +20 -0
  42. package/src/modules/tags/epics.test.ts +95 -0
  43. package/src/modules/tags/index.test.ts +41 -0
  44. package/src/modules/uploads/epics.test.ts +108 -0
  45. package/src/modules/uploads/index.test.ts +58 -0
  46. package/src/operators/checkTagName.test.ts +28 -0
  47. package/src/types/index.ts +23 -7
  48. package/src/utils/blocksToText.test.ts +42 -0
  49. package/src/utils/constructFilter.test.ts +119 -0
  50. package/src/utils/generatePreviewBlobUrl.test.ts +69 -0
  51. package/src/utils/getAssetResolution.test.ts +12 -0
  52. package/src/utils/getDocumentAssetIds.test.ts +49 -0
  53. package/src/utils/getSchemeColor.test.ts +11 -0
  54. package/src/utils/getTagSelectOptions.test.ts +43 -0
  55. package/src/utils/getUniqueDocuments.test.ts +25 -0
  56. package/src/utils/imageDprUrl.test.ts +45 -0
  57. package/src/utils/isSupportedAssetType.test.ts +15 -0
  58. package/src/utils/isSupportedAssetType.ts +15 -0
  59. package/src/utils/sanitizeFormData.test.ts +58 -0
  60. package/src/utils/typeGuards.test.ts +17 -0
  61. package/src/utils/uploadSanityAsset.test.ts +28 -0
  62. package/src/utils/withMaxConcurrency.test.ts +42 -0
package/dist/index.js CHANGED
@@ -467,7 +467,10 @@ const useKeyPress = (hotkey, onPress) => {
467
467
  fn: (value, _field) => value ? `references('${value}')` : void 0,
468
468
  label: "includes"
469
469
  }
470
- }, ORDER_OPTIONS = [
470
+ }, SUPPORTED_ASSET_TYPES = [
471
+ "file",
472
+ "image"
473
+ ], ORDER_OPTIONS = [
471
474
  {
472
475
  direction: "desc",
473
476
  field: "_createdAt"
@@ -539,7 +542,7 @@ const useKeyPress = (hotkey, onPress) => {
539
542
  if (context === void 0)
540
543
  throw new Error("useAssetSourceActions must be used within an AssetSourceDispatchProvider");
541
544
  return context;
542
- }, useVersionedClient = () => sanity.useClient({ apiVersion: "2022-10-01" }), ORDER_DICTIONARY = {
545
+ }, useVersionedClient = () => sanity.useClient({ apiVersion: "2025-10-02" }), ORDER_DICTIONARY = {
543
546
  _createdAt: {
544
547
  asc: "Last created: Oldest first",
545
548
  desc: "Last created: Newest first"
@@ -2362,14 +2365,16 @@ const Container$1 = styledComponents.styled(ui.Box)(({ $scheme, theme }) => styl
2362
2365
  enabled: options?.creditLine?.enabled || !1,
2363
2366
  excludeSources: creditLineExcludeSources
2364
2367
  },
2365
- directUploads: options?.directUploads ?? !0
2368
+ directUploads: options?.directUploads ?? !0,
2369
+ locales: options?.locales
2366
2370
  };
2367
2371
  }, [
2368
2372
  options?.creditLine?.enabled,
2369
2373
  options?.components,
2370
2374
  options?.creditLine?.excludeSources,
2371
2375
  options?.maximumUploadSize,
2372
- options?.directUploads
2376
+ options?.directUploads,
2377
+ options?.locales
2373
2378
  ]);
2374
2379
  return /* @__PURE__ */ jsxRuntime.jsx(ToolOptionsContext.Provider, { value, children });
2375
2380
  }, useToolOptions = () => {
@@ -2648,21 +2653,35 @@ const DebugControls = () => {
2648
2653
  ] })
2649
2654
  }
2650
2655
  ) : null;
2651
- }, tagOptionSchema = z__namespace.object({
2656
+ };
2657
+ function localizedStringSchema(locales) {
2658
+ if (!locales || locales.length === 0)
2659
+ return z__namespace.string().trim().optional();
2660
+ const shape = {};
2661
+ for (const locale of locales)
2662
+ shape[locale.id] = z__namespace.string().trim().optional();
2663
+ return z__namespace.object(shape).passthrough();
2664
+ }
2665
+ const tagOptionSchema = z__namespace.object({
2652
2666
  label: z__namespace.string().trim().min(1, { message: "Label cannot be empty" }),
2653
2667
  value: z__namespace.string().trim().min(1, { message: "Value cannot be empty" })
2654
- }), assetFormSchema = z__namespace.object({
2655
- altText: z__namespace.string().trim().optional(),
2656
- creditLine: z__namespace.string().trim().optional(),
2657
- description: z__namespace.string().trim().optional(),
2658
- opt: z__namespace.object({
2659
- media: z__namespace.object({
2660
- tags: z__namespace.array(tagOptionSchema).nullable()
2661
- })
2662
- }),
2663
- originalFilename: z__namespace.string().trim().min(1, { message: "Filename cannot be empty" }),
2664
- title: z__namespace.string().trim().optional()
2665
- }), tagFormSchema = z__namespace.object({
2668
+ });
2669
+ function getAssetFormSchema(locales) {
2670
+ return z__namespace.object({
2671
+ altText: localizedStringSchema(locales),
2672
+ creditLine: localizedStringSchema(locales),
2673
+ description: localizedStringSchema(locales),
2674
+ opt: z__namespace.object({
2675
+ media: z__namespace.object({
2676
+ tags: z__namespace.array(tagOptionSchema).nullable()
2677
+ })
2678
+ }),
2679
+ originalFilename: z__namespace.string().trim().min(1, { message: "Filename cannot be empty" }),
2680
+ title: localizedStringSchema(locales)
2681
+ });
2682
+ }
2683
+ getAssetFormSchema();
2684
+ const tagFormSchema = z__namespace.object({
2666
2685
  name: z__namespace.string().min(1, { message: "Name cannot be empty" })
2667
2686
  });
2668
2687
  function getUniqueDocuments(documents) {
@@ -3097,6 +3116,11 @@ const imageDprUrl = (asset, options) => {
3097
3116
  )
3098
3117
  ] });
3099
3118
  });
3119
+ function toStringField(value) {
3120
+ if (typeof value == "string") return value;
3121
+ if (typeof value == "object" && value !== null)
3122
+ return Object.values(value).find((v) => v) || void 0;
3123
+ }
3100
3124
  function Details({
3101
3125
  formUpdating,
3102
3126
  handleCreateTag,
@@ -3106,8 +3130,10 @@ function Details({
3106
3130
  allTagOptions,
3107
3131
  assetTagOptions,
3108
3132
  currentAsset,
3109
- creditLine
3133
+ creditLine,
3134
+ locales
3110
3135
  }) {
3136
+ const hasLocales = locales && locales.length > 0, [activeLocaleTab, setActiveLocaleTab] = react.useState(0);
3111
3137
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
3112
3138
  /* @__PURE__ */ jsxRuntime.jsx(
3113
3139
  FormFieldInputTags,
@@ -3134,51 +3160,117 @@ function Details({
3134
3160
  value: currentAsset?.originalFilename
3135
3161
  }
3136
3162
  ),
3137
- /* @__PURE__ */ jsxRuntime.jsx(
3138
- FormFieldInputText,
3139
- {
3140
- ...register("title"),
3141
- disabled: formUpdating,
3142
- error: errors?.title?.message,
3143
- label: "Title",
3144
- name: "title",
3145
- value: currentAsset?.title
3146
- }
3147
- ),
3148
- /* @__PURE__ */ jsxRuntime.jsx(
3149
- FormFieldInputText,
3150
- {
3151
- ...register("altText"),
3152
- disabled: formUpdating,
3153
- error: errors?.altText?.message,
3154
- label: "Alt Text",
3155
- name: "altText",
3156
- value: currentAsset?.altText
3157
- }
3158
- ),
3159
- /* @__PURE__ */ jsxRuntime.jsx(
3160
- FormFieldInputTextarea,
3161
- {
3162
- ...register("description"),
3163
- disabled: formUpdating,
3164
- error: errors?.description?.message,
3165
- label: "Description",
3166
- name: "description",
3167
- rows: 5,
3168
- value: currentAsset?.description
3169
- }
3170
- ),
3171
- creditLine?.enabled && /* @__PURE__ */ jsxRuntime.jsx(
3172
- FormFieldInputText,
3173
- {
3174
- ...register("creditLine"),
3175
- error: errors?.creditLine?.message,
3176
- label: "Credit",
3177
- name: "creditLine",
3178
- value: currentAsset?.creditLine,
3179
- disabled: formUpdating || creditLine?.excludeSources?.includes(currentAsset?.source?.name)
3180
- }
3181
- )
3163
+ hasLocales ? /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { marginTop: 2, shadow: 1, padding: 3, radius: 1, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
3164
+ /* @__PURE__ */ jsxRuntime.jsx(ui.TabList, { space: 2, children: locales.map((locale, idx) => /* @__PURE__ */ jsxRuntime.jsx(
3165
+ ui.Tab,
3166
+ {
3167
+ id: `locale-tab-${locale.id}`,
3168
+ "aria-controls": `locale-panel-${locale.id}`,
3169
+ selected: activeLocaleTab === idx,
3170
+ onClick: () => setActiveLocaleTab(idx),
3171
+ label: locale.title
3172
+ },
3173
+ locale.id
3174
+ )) }),
3175
+ locales.map((locale, idx) => /* @__PURE__ */ jsxRuntime.jsx(
3176
+ ui.TabPanel,
3177
+ {
3178
+ id: `locale-panel-${locale.id}`,
3179
+ "aria-labelledby": `locale-tab-${locale.id}`,
3180
+ hidden: activeLocaleTab !== idx,
3181
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
3182
+ /* @__PURE__ */ jsxRuntime.jsx(
3183
+ FormFieldInputText,
3184
+ {
3185
+ ...register(`title.${locale.id}`),
3186
+ disabled: formUpdating,
3187
+ error: errors?.title?.[locale.id]?.message,
3188
+ label: "Title",
3189
+ name: `title.${locale.id}`
3190
+ }
3191
+ ),
3192
+ /* @__PURE__ */ jsxRuntime.jsx(
3193
+ FormFieldInputText,
3194
+ {
3195
+ ...register(`altText.${locale.id}`),
3196
+ disabled: formUpdating,
3197
+ error: errors?.altText?.[locale.id]?.message,
3198
+ label: "Alt Text",
3199
+ name: `altText.${locale.id}`
3200
+ }
3201
+ ),
3202
+ /* @__PURE__ */ jsxRuntime.jsx(
3203
+ FormFieldInputTextarea,
3204
+ {
3205
+ ...register(`description.${locale.id}`),
3206
+ disabled: formUpdating,
3207
+ error: errors?.description?.[locale.id]?.message,
3208
+ label: "Description",
3209
+ name: `description.${locale.id}`,
3210
+ rows: 5
3211
+ }
3212
+ ),
3213
+ creditLine?.enabled && /* @__PURE__ */ jsxRuntime.jsx(
3214
+ FormFieldInputText,
3215
+ {
3216
+ ...register(`creditLine.${locale.id}`),
3217
+ error: errors?.creditLine?.[locale.id]?.message,
3218
+ label: "Credit",
3219
+ name: `creditLine.${locale.id}`,
3220
+ disabled: formUpdating || creditLine?.excludeSources?.includes(currentAsset?.source?.name)
3221
+ }
3222
+ )
3223
+ ] })
3224
+ },
3225
+ locale.id
3226
+ ))
3227
+ ] }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3228
+ /* @__PURE__ */ jsxRuntime.jsx(
3229
+ FormFieldInputText,
3230
+ {
3231
+ ...register("title"),
3232
+ disabled: formUpdating,
3233
+ error: errors?.title?.message,
3234
+ label: "Title",
3235
+ name: "title",
3236
+ value: toStringField(currentAsset?.title)
3237
+ }
3238
+ ),
3239
+ /* @__PURE__ */ jsxRuntime.jsx(
3240
+ FormFieldInputText,
3241
+ {
3242
+ ...register("altText"),
3243
+ disabled: formUpdating,
3244
+ error: errors?.altText?.message,
3245
+ label: "Alt Text",
3246
+ name: "altText",
3247
+ value: toStringField(currentAsset?.altText)
3248
+ }
3249
+ ),
3250
+ /* @__PURE__ */ jsxRuntime.jsx(
3251
+ FormFieldInputTextarea,
3252
+ {
3253
+ ...register("description"),
3254
+ disabled: formUpdating,
3255
+ error: errors?.description?.message,
3256
+ label: "Description",
3257
+ name: "description",
3258
+ rows: 5,
3259
+ value: toStringField(currentAsset?.description)
3260
+ }
3261
+ ),
3262
+ creditLine?.enabled && /* @__PURE__ */ jsxRuntime.jsx(
3263
+ FormFieldInputText,
3264
+ {
3265
+ ...register("creditLine"),
3266
+ error: errors?.creditLine?.message,
3267
+ label: "Credit",
3268
+ name: "creditLine",
3269
+ value: toStringField(currentAsset?.creditLine),
3270
+ disabled: formUpdating || creditLine?.excludeSources?.includes(currentAsset?.source?.name)
3271
+ }
3272
+ )
3273
+ ] })
3182
3274
  ] });
3183
3275
  }
3184
3276
  function renderDefaultDetails(props) {
@@ -3188,16 +3280,37 @@ const DialogAssetEdit = (props) => {
3188
3280
  const {
3189
3281
  children,
3190
3282
  dialog: { assetId, id, lastCreatedTag, lastRemovedTagIds }
3191
- } = props, client = useVersionedClient(), scheme = sanity.useColorSchemeValue(), documentStore = sanity.useDocumentStore(), dispatch = reactRedux.useDispatch(), assetItem = useTypedSelector((state) => selectAssetById(state, String(assetId))), tags = useTypedSelector(selectTags), assetUpdatedPrev = react.useRef(void 0), [assetSnapshot, setAssetSnapshot] = react.useState(assetItem?.asset), [tabSection, setTabSection] = react.useState("details"), currentAsset = assetItem ? assetItem?.asset : assetSnapshot, allTagOptions = getTagSelectOptions(tags), assetTagOptions = useTypedSelector(selectTagSelectOptions(currentAsset)), { creditLine, components: { details: CustomDetails } = {} } = useToolOptions(), generateDefaultValues = react.useCallback(
3192
- (asset) => ({
3193
- altText: asset?.altText || "",
3194
- creditLine: asset?.creditLine || "",
3195
- description: asset?.description || "",
3196
- originalFilename: asset?.originalFilename || "",
3197
- opt: { media: { tags: assetTagOptions } },
3198
- title: asset?.title || ""
3199
- }),
3200
- [assetTagOptions]
3283
+ } = props, client = useVersionedClient(), scheme = sanity.useColorSchemeValue(), documentStore = sanity.useDocumentStore(), dispatch = reactRedux.useDispatch(), assetItem = useTypedSelector((state) => selectAssetById(state, String(assetId))), tags = useTypedSelector(selectTags), assetUpdatedPrev = react.useRef(void 0), [assetSnapshot, setAssetSnapshot] = react.useState(assetItem?.asset), [tabSection, setTabSection] = react.useState("details"), currentAsset = assetItem ? assetItem?.asset : assetSnapshot, allTagOptions = getTagSelectOptions(tags), assetTagOptions = useTypedSelector(selectTagSelectOptions(currentAsset)), { creditLine, components: { details: CustomDetails } = {}, locales } = useToolOptions(), generateDefaultValues = react.useCallback(
3284
+ (asset) => {
3285
+ if (locales && locales.length > 0) {
3286
+ const makeLocaleObj = (field) => {
3287
+ const obj = {};
3288
+ for (let i = 0; i < locales.length; i++) {
3289
+ const locale = locales[i];
3290
+ typeof field == "object" && field && field[locale.id] ? obj[locale.id] = field[locale.id] : typeof field == "string" ? obj[locale.id] = i === 0 ? field : "" : obj[locale.id] = "";
3291
+ }
3292
+ return obj;
3293
+ };
3294
+ return {
3295
+ altText: makeLocaleObj(asset?.altText),
3296
+ creditLine: makeLocaleObj(asset?.creditLine),
3297
+ description: makeLocaleObj(asset?.description),
3298
+ originalFilename: asset?.originalFilename || "",
3299
+ opt: { media: { tags: assetTagOptions } },
3300
+ title: makeLocaleObj(asset?.title)
3301
+ };
3302
+ }
3303
+ const flattenField = (field) => typeof field == "string" ? field : typeof field == "object" && field !== null && Object.values(field).find((v) => v) || "";
3304
+ return {
3305
+ altText: flattenField(asset?.altText),
3306
+ creditLine: flattenField(asset?.creditLine),
3307
+ description: flattenField(asset?.description),
3308
+ originalFilename: asset?.originalFilename || "",
3309
+ opt: { media: { tags: assetTagOptions } },
3310
+ title: flattenField(asset?.title)
3311
+ };
3312
+ },
3313
+ [assetTagOptions, locales]
3201
3314
  ), {
3202
3315
  control,
3203
3316
  // Read the formState before render to subscribe the form state through Proxy
@@ -3210,7 +3323,7 @@ const DialogAssetEdit = (props) => {
3210
3323
  } = reactHookForm.useForm({
3211
3324
  defaultValues: generateDefaultValues(assetItem?.asset),
3212
3325
  mode: "onChange",
3213
- resolver: zod.zodResolver(assetFormSchema)
3326
+ resolver: zod.zodResolver(getAssetFormSchema(locales))
3214
3327
  }), formUpdating = !assetItem || assetItem?.updating, handleClose = react.useCallback(() => {
3215
3328
  dispatch(dialogActions.remove({ id }));
3216
3329
  }, [dispatch, id]), handleDelete = react.useCallback(() => {
@@ -3233,7 +3346,39 @@ const DialogAssetEdit = (props) => {
3233
3346
  );
3234
3347
  },
3235
3348
  [currentAsset?._id, dispatch]
3236
- ), onSubmit = react.useCallback(
3349
+ ), hasOrphanedLocales = react.useMemo(() => {
3350
+ if (!currentAsset) return !1;
3351
+ const isLocaleObj = (v) => typeof v == "object" && v !== null && !Array.isArray(v), fields = [
3352
+ currentAsset.title,
3353
+ currentAsset.altText,
3354
+ currentAsset.description,
3355
+ ...currentAsset._type === "sanity.imageAsset" ? [currentAsset.creditLine] : []
3356
+ ];
3357
+ if (!fields.some((f) => isLocaleObj(f))) return !1;
3358
+ if (!locales || locales.length === 0) return !0;
3359
+ const configuredIds = new Set(locales.map((l) => l.id));
3360
+ return fields.some((f) => isLocaleObj(f) ? Object.keys(f).some((k) => !configuredIds.has(k)) : !1);
3361
+ }, [currentAsset, locales]), handleCleanupLocales = react.useCallback(async () => {
3362
+ if (!currentAsset) return;
3363
+ const cleanField = (field) => {
3364
+ if (typeof field != "object" || field === null || Array.isArray(field)) return field;
3365
+ const obj = field;
3366
+ if (!locales || locales.length === 0)
3367
+ return Object.keys(obj).sort().map((k) => obj[k]).find((v) => v) || "";
3368
+ const configuredIds = new Set(locales.map((l) => l.id)), cleaned = {};
3369
+ for (const [key, val] of Object.entries(obj))
3370
+ configuredIds.has(key) && (cleaned[key] = val);
3371
+ return cleaned;
3372
+ };
3373
+ await client.patch(currentAsset._id).set({
3374
+ title: cleanField(currentAsset.title),
3375
+ altText: cleanField(currentAsset.altText),
3376
+ description: cleanField(currentAsset.description),
3377
+ ...currentAsset._type === "sanity.imageAsset" && {
3378
+ creditLine: cleanField(currentAsset.creditLine)
3379
+ }
3380
+ }).commit();
3381
+ }, [client, currentAsset, locales]), onSubmit = react.useCallback(
3237
3382
  (formData) => {
3238
3383
  if (!assetItem?.asset)
3239
3384
  return;
@@ -3281,27 +3426,42 @@ const DialogAssetEdit = (props) => {
3281
3426
  }, [getValues, lastRemovedTagIds, setValue]), react.useEffect(() => {
3282
3427
  assetUpdatedPrev.current !== assetItem?.asset._updatedAt && reset(generateDefaultValues(assetItem?.asset)), assetUpdatedPrev.current = assetItem?.asset._updatedAt;
3283
3428
  }, [assetItem?.asset, generateDefaultValues, reset]);
3284
- const Footer = () => /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "space-between", children: [
3285
- /* @__PURE__ */ jsxRuntime.jsx(
3286
- ui.Button,
3287
- {
3288
- disabled: formUpdating,
3289
- fontSize: 1,
3290
- mode: "bleed",
3291
- onClick: handleDelete,
3292
- text: "Delete",
3293
- tone: "critical"
3294
- }
3295
- ),
3296
- /* @__PURE__ */ jsxRuntime.jsx(
3297
- FormSubmitButton,
3298
- {
3299
- disabled: formUpdating || !isDirty || !isValid,
3300
- isValid,
3301
- lastUpdated: currentAsset?._updatedAt,
3302
- onClick: handleSubmit(onSubmit)
3303
- }
3304
- )
3429
+ const Footer = () => /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
3430
+ hasOrphanedLocales && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, radius: 2, shadow: 1, tone: "caution", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", justify: "space-between", gap: 3, children: [
3431
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: "This asset has localized fields that are no longer configured. Clean them up to avoid validation errors." }),
3432
+ /* @__PURE__ */ jsxRuntime.jsx(
3433
+ ui.Button,
3434
+ {
3435
+ fontSize: 1,
3436
+ mode: "ghost",
3437
+ onClick: handleCleanupLocales,
3438
+ text: "Cleanup localized fields",
3439
+ tone: "caution"
3440
+ }
3441
+ )
3442
+ ] }) }),
3443
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "space-between", children: [
3444
+ /* @__PURE__ */ jsxRuntime.jsx(
3445
+ ui.Button,
3446
+ {
3447
+ disabled: formUpdating,
3448
+ fontSize: 1,
3449
+ mode: "bleed",
3450
+ onClick: handleDelete,
3451
+ text: "Delete",
3452
+ tone: "critical"
3453
+ }
3454
+ ),
3455
+ /* @__PURE__ */ jsxRuntime.jsx(
3456
+ FormSubmitButton,
3457
+ {
3458
+ disabled: formUpdating || !isDirty || !isValid || hasOrphanedLocales,
3459
+ isValid,
3460
+ lastUpdated: currentAsset?._updatedAt,
3461
+ onClick: handleSubmit(onSubmit)
3462
+ }
3463
+ )
3464
+ ] })
3305
3465
  ] }) });
3306
3466
  if (!currentAsset)
3307
3467
  return null;
@@ -3315,7 +3475,8 @@ const DialogAssetEdit = (props) => {
3315
3475
  allTagOptions,
3316
3476
  handleCreateTag,
3317
3477
  currentAsset,
3318
- creditLine
3478
+ creditLine,
3479
+ locales
3319
3480
  };
3320
3481
  return /* @__PURE__ */ jsxRuntime.jsxs(
3321
3482
  Dialog,
@@ -4009,7 +4170,7 @@ const DialogAssetEdit = (props) => {
4009
4170
  `
4010
4171
  ), StyledWarningOutlineIcon = styledComponents.styled(icons.WarningFilledIcon)(({ theme }) => ({
4011
4172
  color: theme.sanity.color.spot.red
4012
- })), CardAsset = (props) => {
4173
+ })), CardAsset$1 = (props) => {
4013
4174
  const { id, selected } = props, scheme = sanity.useColorSchemeValue(), shiftPressed = useKeyPress("shift"), dispatch = reactRedux.useDispatch(), lastPicked = useTypedSelector((state) => state.assets.lastPicked), item = useTypedSelector((state) => selectAssetById(state, id)), asset = item?.asset, error = item?.error, isOpaque = item?.asset?.metadata?.isOpaque, picked = item?.picked, updating = item?.updating, { onSelect } = useAssetSourceActions();
4014
4175
  if (!asset)
4015
4176
  return null;
@@ -4139,7 +4300,7 @@ const DialogAssetEdit = (props) => {
4139
4300
  )
4140
4301
  ] }) });
4141
4302
  };
4142
- var CardAsset$1 = react.memo(CardAsset);
4303
+ var CardAsset = react.memo(CardAsset$1);
4143
4304
  const PREVIEW_WIDTH = 180, createBlob = (img) => new Promise((resolve) => {
4144
4305
  const imageAspect = img.width / img.height, canvas = document.createElement("canvas");
4145
4306
  canvas.width = PREVIEW_WIDTH, canvas.height = Math.max(PREVIEW_WIDTH / imageAspect, 1);
@@ -4496,7 +4657,7 @@ const CardWrapper = styledComponents.styled(ui.Flex)`
4496
4657
  }
4497
4658
  ) });
4498
4659
  }, CARD_HEIGHT = 220, CARD_WIDTH = 240, VirtualCell = react.memo(
4499
- ({ item, selected }) => item?.type === "asset" ? /* @__PURE__ */ jsxRuntime.jsx(CardAsset$1, { id: item.id, selected }) : item?.type === "upload" ? /* @__PURE__ */ jsxRuntime.jsx(CardUpload, { id: item.id }) : null
4660
+ ({ item, selected }) => item?.type === "asset" ? /* @__PURE__ */ jsxRuntime.jsx(CardAsset, { id: item.id, selected }) : item?.type === "upload" ? /* @__PURE__ */ jsxRuntime.jsx(CardUpload, { id: item.id }) : null
4500
4661
  ), StyledItemContainer = styledComponents.styled.div`
4501
4662
  height: ${CARD_HEIGHT}px;
4502
4663
  width: ${CARD_WIDTH}px;
@@ -4661,7 +4822,7 @@ const CardWrapper = styledComponents.styled(ui.Flex)`
4661
4822
  `
4662
4823
  ), StyledWarningIcon = styledComponents.styled(icons.WarningFilledIcon)(({ theme }) => ({
4663
4824
  color: theme.sanity.color.spot.red
4664
- })), TableRowAsset = (props) => {
4825
+ })), TableRowAsset$1 = (props) => {
4665
4826
  const { id, selected } = props, scheme = sanity.useColorSchemeValue(), shiftPressed = useKeyPress("shift"), [referenceCountVisible, setReferenceCountVisible] = react.useState(!1), refCountVisibleTimeout = react.useRef(null), dispatch = reactRedux.useDispatch(), lastPicked = useTypedSelector((state) => state.assets.lastPicked), item = useTypedSelector((state) => selectAssetById(state, id)), mediaIndex = ui.useMediaIndex(), asset = item?.asset, error = item?.error, isOpaque = item?.asset?.metadata?.isOpaque, picked = item?.picked, updating = item?.updating, { onSelect } = useAssetSourceActions(), handleContextActionClick = react.useCallback(
4666
4827
  (e) => {
4667
4828
  e.stopPropagation(), asset && (onSelect ? dispatch(dialogActions.showAssetEdit({ assetId: asset._id })) : shiftPressed.current && !picked ? dispatch(assetsActions.pickRange({ startId: lastPicked || asset._id, endId: asset._id })) : dispatch(assetsActions.pick({ assetId: asset._id, picked: !picked })));
@@ -4887,7 +5048,7 @@ const CardWrapper = styledComponents.styled(ui.Flex)`
4887
5048
  }
4888
5049
  ) : null;
4889
5050
  };
4890
- var TableRowAsset$1 = react.memo(TableRowAsset);
5051
+ var TableRowAsset = react.memo(TableRowAsset$1);
4891
5052
  const TableRowUpload = (props) => {
4892
5053
  const { id } = props, scheme = sanity.useColorSchemeValue(), dispatch = reactRedux.useDispatch(), item = useTypedSelector((state) => selectUploadById(state, id)), mediaIndex = ui.useMediaIndex();
4893
5054
  if (!item)
@@ -5001,7 +5162,7 @@ const TableRowUpload = (props) => {
5001
5162
  }
5002
5163
  );
5003
5164
  }, VirtualRow = react.memo(
5004
- ({ item, selected }) => item?.type === "asset" ? /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { style: { height: "100px" }, children: /* @__PURE__ */ jsxRuntime.jsx(TableRowAsset$1, { id: item.id, selected }) }) : item?.type === "upload" ? /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { style: { height: "100px" }, children: /* @__PURE__ */ jsxRuntime.jsx(TableRowUpload, { id: item.id }) }) : null
5165
+ ({ item, selected }) => item?.type === "asset" ? /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { style: { height: "100px" }, children: /* @__PURE__ */ jsxRuntime.jsx(TableRowAsset, { id: item.id, selected }) }) : item?.type === "upload" ? /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { style: { height: "100px" }, children: /* @__PURE__ */ jsxRuntime.jsx(TableRowUpload, { id: item.id }) }) : null
5005
5166
  ), AssetTableVirtualized = (props) => {
5006
5167
  const { items, onLoadMore } = props, selectedAssets = useTypedSelector((state) => state.selected.assets), selectedIds = selectedAssets && selectedAssets.map((asset) => asset._id) || [], totalCount = items?.length;
5007
5168
  return totalCount === 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(
@@ -5101,6 +5262,9 @@ const TableRowUpload = (props) => {
5101
5262
  reducers: {}
5102
5263
  });
5103
5264
  var selectedReducer = selectedSlice.reducer;
5265
+ function messageFromGenericErrorPayload(payload) {
5266
+ return !payload || typeof payload != "object" ? "Unknown error" : "error" in payload && payload.error && typeof payload.error == "object" && payload.error !== null && "message" in payload.error ? String(payload.error.message) : "message" in payload && typeof payload.message == "string" ? String(payload.message) : "Unknown error";
5267
+ }
5104
5268
  const initialState = {
5105
5269
  items: []
5106
5270
  }, notificationsSlice = toolkit.createSlice({
@@ -5187,11 +5351,11 @@ const initialState = {
5187
5351
  uploadsActions.uploadError.type
5188
5352
  ),
5189
5353
  operators$1.mergeMap((action) => {
5190
- const error = action.payload?.error;
5354
+ const title = `An error occurred: ${messageFromGenericErrorPayload(action.payload)}`;
5191
5355
  return rxjs.of(
5192
5356
  notificationsSlice.actions.add({
5193
5357
  status: "error",
5194
- title: `An error occured: ${error.message}`
5358
+ title
5195
5359
  })
5196
5360
  );
5197
5361
  })
@@ -5264,6 +5428,9 @@ const rootEpic = reduxObservable.combineEpics(
5264
5428
  const assetIds = getAssetIds(document2);
5265
5429
  return [...new Set(assetIds.sort())];
5266
5430
  };
5431
+ function isSupportedAssetType(assetType) {
5432
+ return assetType ? SUPPORTED_ASSET_TYPES.includes(assetType) : !1;
5433
+ }
5267
5434
  class ReduxProvider extends react.Component {
5268
5435
  store;
5269
5436
  constructor(props) {
@@ -5294,7 +5461,7 @@ class ReduxProvider extends react.Component {
5294
5461
  preloadedState: {
5295
5462
  assets: {
5296
5463
  ...initialState$5,
5297
- assetTypes: props?.assetType ? [props.assetType] : ["file", "image"]
5464
+ assetTypes: isSupportedAssetType(props?.assetType) ? [props.assetType] : ["file", "image"]
5298
5465
  },
5299
5466
  debug: {
5300
5467
  badConnection: !1,